forgesmith 0.1.0 → 0.3.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/CHANGELOG.md +27 -0
- package/dist/index.cjs +370 -0
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.mjs +365 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — Ask-Driven Asset Generation
|
|
4
|
+
|
|
5
|
+
- `generateAskDrivenAsset(blueprint, question, opts, provider)` — generate any content format from a natural-language question about your codebase architecture
|
|
6
|
+
- Formats: `markdown` | `blog` | `social` | `email` | `slack` | `slide`
|
|
7
|
+
- Tones: `professional` | `casual` | `technical` | `executive`
|
|
8
|
+
- Grounded in Blueprint dependency graph (top files by usage, edge sample, category breakdown)
|
|
9
|
+
- Format-aware token budgets (512 for social/slack, 3072 for blog/markdown, 2048 for slides)
|
|
10
|
+
- Use-cases: blog posts, tweets, investor-update bullets, Slack status, slide decks
|
|
11
|
+
- New exported types: `AskDrivenAssetOpts`, `AskDrivenAssetFormat`
|
|
12
|
+
- 52 unit tests, all green (+8 new)
|
|
13
|
+
|
|
14
|
+
## 0.2.0 — Architecture Narrative Generators
|
|
15
|
+
|
|
16
|
+
- `generateArchitectureWalkthrough(blueprint, opts, provider)` — long-form narrative explaining structure, key files, and important relationships. Tones: `technical` | `casual` | `onboarding`
|
|
17
|
+
- `generateChangesSince(current, previous, opts, provider)` — delta narrative: what changed between two Blueprint snapshots (new/removed files, edge changes, stats delta)
|
|
18
|
+
- `generateOnboardingDoc(blueprint, opts, provider)` — new-developer intro to codebase with "Start here", key folders, core shared files. Tones: `friendly` | `formal` | `technical`
|
|
19
|
+
- `generateRefactoringReport(blueprint, opts, provider)` — identifies architecture debt (hot spots, high fan-out, import cycles), suggests refactoring priorities. Tones: `analytical` | `casual` | `executive`
|
|
20
|
+
- `readBlueprintData(targetPath)` — reads `.prism/blueprint/snapshot.json`; returns `BlueprintData | null`
|
|
21
|
+
- New exported types: `BlueprintData`, `BlueprintFile`, `BlueprintEdge`, `ArchitectureWalkthroughOpts`, `ChangesSinceOpts`, `OnboardingDocOpts`, `RefactoringReportOpts`
|
|
22
|
+
- 44 unit tests, all green (+31 new)
|
|
23
|
+
|
|
24
|
+
## 0.1.1 — /providers sub-entry fix
|
|
25
|
+
|
|
26
|
+
- Add explicit `./providers` export entry to package.json exports map
|
|
27
|
+
- Add `src/providers.ts` barrel (re-exports `DirectLlmProvider`)
|
|
28
|
+
- Fixes `Module not found: Can't resolve 'forgesmith/providers'` for consumers
|
|
29
|
+
|
|
3
30
|
## 0.1.0 — Initial real release
|
|
4
31
|
|
|
5
32
|
- `generateReleaseNotes(data, opts, provider)` — LLM-powered release note generation from prism0x2A data
|
package/dist/index.cjs
CHANGED
|
@@ -72,6 +72,279 @@ async function generateReleaseNotes(data, opts, provider) {
|
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
// src/generators/architectureWalkthrough.ts
|
|
77
|
+
var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
|
|
78
|
+
function buildSystemPrompt2(tone) {
|
|
79
|
+
const toneMap = {
|
|
80
|
+
technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
|
|
81
|
+
casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
|
|
82
|
+
onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
|
|
83
|
+
};
|
|
84
|
+
return `${toneMap[tone] ?? toneMap.technical} Write only the requested document \u2014 no preamble, no meta-commentary.`;
|
|
85
|
+
}
|
|
86
|
+
function buildUserPrompt2(blueprint, opts) {
|
|
87
|
+
const tone = opts.tone ?? "technical";
|
|
88
|
+
const length = opts.length ?? "long";
|
|
89
|
+
const format = opts.format ?? "markdown";
|
|
90
|
+
const lengthGuide = {
|
|
91
|
+
short: "3-4 sections, concise overview",
|
|
92
|
+
medium: "5-7 sections with moderate detail",
|
|
93
|
+
long: "comprehensive, 8+ sections with full detail and examples"
|
|
94
|
+
}[length];
|
|
95
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 15);
|
|
96
|
+
const edgeSample = blueprint.edges.slice(0, 20);
|
|
97
|
+
const lines = [
|
|
98
|
+
`Generate an architecture walkthrough document in ${format} format for the codebase at "${blueprint.targetPath}".`,
|
|
99
|
+
`Tone: ${tone}. Length: ${length} (${lengthGuide}).`,
|
|
100
|
+
``,
|
|
101
|
+
`## Codebase Stats`,
|
|
102
|
+
`- Total files: ${blueprint.stats.totalFiles}`,
|
|
103
|
+
`- Runtime dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
104
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
105
|
+
``,
|
|
106
|
+
`## Key Files (by incoming dependency count)`,
|
|
107
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, imports: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
108
|
+
``,
|
|
109
|
+
`## Dependency Edge Sample`,
|
|
110
|
+
...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`),
|
|
111
|
+
``,
|
|
112
|
+
`Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
|
|
113
|
+
];
|
|
114
|
+
return lines.join("\n");
|
|
115
|
+
}
|
|
116
|
+
async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
117
|
+
if (!blueprint) {
|
|
118
|
+
return {
|
|
119
|
+
text: NO_DATA_MSG,
|
|
120
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const response = await provider.complete({
|
|
124
|
+
systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
|
|
125
|
+
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
|
|
126
|
+
maxTokens: 4096
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
text: response.content,
|
|
130
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/generators/changesSince.ts
|
|
135
|
+
var NO_DATA_MSG2 = "No Blueprint data available. Run prism scan first.";
|
|
136
|
+
function buildSystemPrompt3() {
|
|
137
|
+
return `You are a technical writer who produces clear, concise architecture change summaries for development teams. Focus on what changed and why it matters. Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
138
|
+
}
|
|
139
|
+
function computeDelta(current, previous) {
|
|
140
|
+
const currentPaths = new Set(current.files.map((f) => f.path));
|
|
141
|
+
const previousPaths = new Set(previous.files.map((f) => f.path));
|
|
142
|
+
const added = current.files.filter((f) => !previousPaths.has(f.path));
|
|
143
|
+
const removed = previous.files.filter((f) => !currentPaths.has(f.path));
|
|
144
|
+
const currentEdgeKeys = new Set(current.edges.map((e) => `${e.from}\u2192${e.to}`));
|
|
145
|
+
const previousEdgeKeys = new Set(previous.edges.map((e) => `${e.from}\u2192${e.to}`));
|
|
146
|
+
const addedEdges = current.edges.filter((e) => !previousEdgeKeys.has(`${e.from}\u2192${e.to}`));
|
|
147
|
+
const removedEdges = previous.edges.filter((e) => !currentEdgeKeys.has(`${e.from}\u2192${e.to}`));
|
|
148
|
+
return { added, removed, addedEdges, removedEdges };
|
|
149
|
+
}
|
|
150
|
+
function buildUserPrompt3(current, previous, opts) {
|
|
151
|
+
const tone = opts.tone ?? "professional";
|
|
152
|
+
const length = opts.length ?? "medium";
|
|
153
|
+
const format = opts.format ?? "markdown";
|
|
154
|
+
const delta = computeDelta(current, previous);
|
|
155
|
+
const prevDate = new Date(previous.scanTimestamp).toISOString().split("T")[0];
|
|
156
|
+
const currDate = new Date(current.scanTimestamp).toISOString().split("T")[0];
|
|
157
|
+
const lines = [
|
|
158
|
+
`Generate an architecture change summary in ${format} format.`,
|
|
159
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
160
|
+
`Period: ${prevDate} \u2192 ${currDate}`,
|
|
161
|
+
``,
|
|
162
|
+
`## File Changes`,
|
|
163
|
+
`- Added files (${delta.added.length}): ${delta.added.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
|
|
164
|
+
`- Removed files (${delta.removed.length}): ${delta.removed.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
|
|
165
|
+
``,
|
|
166
|
+
`## Dependency Edge Changes`,
|
|
167
|
+
`- New edges (${delta.addedEdges.length}): ${delta.addedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
|
|
168
|
+
`- Removed edges (${delta.removedEdges.length}): ${delta.removedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
|
|
169
|
+
``,
|
|
170
|
+
`## Stats Delta`,
|
|
171
|
+
`- Files: ${previous.stats.totalFiles} \u2192 ${current.stats.totalFiles} (${current.stats.totalFiles - previous.stats.totalFiles >= 0 ? "+" : ""}${current.stats.totalFiles - previous.stats.totalFiles})`,
|
|
172
|
+
`- Edges: ${previous.stats.runtimeEdges} \u2192 ${current.stats.runtimeEdges} (${current.stats.runtimeEdges - previous.stats.runtimeEdges >= 0 ? "+" : ""}${current.stats.runtimeEdges - previous.stats.runtimeEdges})`,
|
|
173
|
+
``,
|
|
174
|
+
`Summarize what changed architecturally, highlight significant additions or removals, and note any structural shifts.`
|
|
175
|
+
];
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
async function generateChangesSince(current, previous, opts, provider) {
|
|
179
|
+
if (!current || !previous) {
|
|
180
|
+
return {
|
|
181
|
+
text: NO_DATA_MSG2,
|
|
182
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
const response = await provider.complete({
|
|
186
|
+
systemPrompt: buildSystemPrompt3(),
|
|
187
|
+
messages: [{ role: "user", content: buildUserPrompt3(current, previous, opts) }],
|
|
188
|
+
maxTokens: 2048
|
|
189
|
+
});
|
|
190
|
+
return {
|
|
191
|
+
text: response.content,
|
|
192
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/generators/onboardingDoc.ts
|
|
197
|
+
var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
|
|
198
|
+
function buildSystemPrompt4(tone) {
|
|
199
|
+
const toneMap = {
|
|
200
|
+
friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
201
|
+
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams.",
|
|
202
|
+
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
|
|
203
|
+
};
|
|
204
|
+
return `${toneMap[tone] ?? toneMap.friendly} Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
205
|
+
}
|
|
206
|
+
function buildUserPrompt4(blueprint, opts) {
|
|
207
|
+
const tone = opts.tone ?? "friendly";
|
|
208
|
+
const length = opts.length ?? "medium";
|
|
209
|
+
const format = opts.format ?? "markdown";
|
|
210
|
+
const topEntryPoints = blueprint.files.filter((f) => f.category === "app" || (f.importedByCount ?? 0) === 0).slice(0, 10);
|
|
211
|
+
const topImported = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
|
|
212
|
+
const folderMap = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const f of blueprint.files) {
|
|
214
|
+
const parts = f.path.split("/");
|
|
215
|
+
if (parts.length > 1) {
|
|
216
|
+
const folder = parts[0];
|
|
217
|
+
folderMap.set(folder, (folderMap.get(folder) ?? 0) + 1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const topFolders = [...folderMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([folder, count]) => `${folder}/ (${count} files)`);
|
|
221
|
+
const lines = [
|
|
222
|
+
`Generate an onboarding document in ${format} format for new developers joining this codebase.`,
|
|
223
|
+
`Project path: "${blueprint.targetPath}"`,
|
|
224
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
225
|
+
``,
|
|
226
|
+
`## Codebase Overview`,
|
|
227
|
+
`- ${blueprint.stats.totalFiles} files across ${Object.keys(blueprint.categories).length} categories`,
|
|
228
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
229
|
+
``,
|
|
230
|
+
`## Top-level Folders`,
|
|
231
|
+
...topFolders.map((f) => `- ${f}`),
|
|
232
|
+
``,
|
|
233
|
+
`## Entry Points (app-layer files, zero incoming deps)`,
|
|
234
|
+
...topEntryPoints.map((f) => `- ${f.path}`),
|
|
235
|
+
``,
|
|
236
|
+
`## Core Shared Files (most imported)`,
|
|
237
|
+
...topImported.map((f) => `- ${f.path} \u2014 used by ${f.importedByCount ?? 0} files`),
|
|
238
|
+
``,
|
|
239
|
+
`Include: "Start here" section, key folders tour, important conventions to follow, the 3-5 files to read first, and how to run/test the project.`
|
|
240
|
+
];
|
|
241
|
+
return lines.join("\n");
|
|
242
|
+
}
|
|
243
|
+
async function generateOnboardingDoc(blueprint, opts, provider) {
|
|
244
|
+
if (!blueprint) {
|
|
245
|
+
return {
|
|
246
|
+
text: NO_DATA_MSG3,
|
|
247
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
const response = await provider.complete({
|
|
251
|
+
systemPrompt: buildSystemPrompt4(opts.tone ?? "friendly"),
|
|
252
|
+
messages: [{ role: "user", content: buildUserPrompt4(blueprint, opts) }],
|
|
253
|
+
maxTokens: 3072
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
text: response.content,
|
|
257
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/generators/refactoringReport.ts
|
|
262
|
+
var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
|
|
263
|
+
function buildSystemPrompt5(tone) {
|
|
264
|
+
const toneMap = {
|
|
265
|
+
analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
|
|
266
|
+
casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
|
|
267
|
+
executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership."
|
|
268
|
+
};
|
|
269
|
+
return `${toneMap[tone] ?? toneMap.analytical} Write only the report \u2014 no preamble, no meta-commentary.`;
|
|
270
|
+
}
|
|
271
|
+
function detectImportCycles(edges) {
|
|
272
|
+
const graph = /* @__PURE__ */ new Map();
|
|
273
|
+
for (const e of edges) {
|
|
274
|
+
if (!graph.has(e.from)) graph.set(e.from, /* @__PURE__ */ new Set());
|
|
275
|
+
graph.get(e.from).add(e.to);
|
|
276
|
+
}
|
|
277
|
+
const cycles = [];
|
|
278
|
+
const visited = /* @__PURE__ */ new Set();
|
|
279
|
+
const stack = /* @__PURE__ */ new Set();
|
|
280
|
+
function dfs(node, path2) {
|
|
281
|
+
if (cycles.length >= 5) return;
|
|
282
|
+
if (stack.has(node)) {
|
|
283
|
+
const cycleStart = path2.indexOf(node);
|
|
284
|
+
if (cycleStart !== -1) {
|
|
285
|
+
cycles.push(path2.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (visited.has(node)) return;
|
|
290
|
+
visited.add(node);
|
|
291
|
+
stack.add(node);
|
|
292
|
+
for (const neighbor of graph.get(node) ?? []) {
|
|
293
|
+
dfs(neighbor, [...path2, node]);
|
|
294
|
+
}
|
|
295
|
+
stack.delete(node);
|
|
296
|
+
}
|
|
297
|
+
for (const node of graph.keys()) {
|
|
298
|
+
if (!visited.has(node)) dfs(node, []);
|
|
299
|
+
}
|
|
300
|
+
return cycles;
|
|
301
|
+
}
|
|
302
|
+
function buildUserPrompt5(blueprint, opts) {
|
|
303
|
+
const tone = opts.tone ?? "analytical";
|
|
304
|
+
const length = opts.length ?? "medium";
|
|
305
|
+
const format = opts.format ?? "markdown";
|
|
306
|
+
const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
|
|
307
|
+
const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
|
|
308
|
+
const importCycles = detectImportCycles(blueprint.edges);
|
|
309
|
+
const lines = [
|
|
310
|
+
`Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
|
|
311
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
312
|
+
``,
|
|
313
|
+
`## Architecture Metrics`,
|
|
314
|
+
`- Total files: ${blueprint.stats.totalFiles}`,
|
|
315
|
+
`- Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
316
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
317
|
+
``,
|
|
318
|
+
`## High-Coupling Hot Spots (high total connections)`,
|
|
319
|
+
...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
320
|
+
``,
|
|
321
|
+
`## High Fan-Out Files (many outgoing dependencies)`,
|
|
322
|
+
...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
|
|
323
|
+
``,
|
|
324
|
+
`## Import Cycles Detected (${importCycles.length})`,
|
|
325
|
+
...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
|
|
326
|
+
``,
|
|
327
|
+
`Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
|
|
328
|
+
];
|
|
329
|
+
return lines.join("\n");
|
|
330
|
+
}
|
|
331
|
+
async function generateRefactoringReport(blueprint, opts, provider) {
|
|
332
|
+
if (!blueprint) {
|
|
333
|
+
return {
|
|
334
|
+
text: NO_DATA_MSG4,
|
|
335
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const response = await provider.complete({
|
|
339
|
+
systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
|
|
340
|
+
messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts) }],
|
|
341
|
+
maxTokens: 3072
|
|
342
|
+
});
|
|
343
|
+
return {
|
|
344
|
+
text: response.content,
|
|
345
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
346
|
+
};
|
|
347
|
+
}
|
|
75
348
|
async function readJsonFiles(dir) {
|
|
76
349
|
try {
|
|
77
350
|
const entries = await fs__default.default.readdir(dir);
|
|
@@ -100,6 +373,103 @@ async function readPrismDirectory(prismPath) {
|
|
|
100
373
|
]);
|
|
101
374
|
return { sessions, recommendations, insights };
|
|
102
375
|
}
|
|
376
|
+
async function readBlueprintData(targetPath) {
|
|
377
|
+
const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
378
|
+
try {
|
|
379
|
+
const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
|
|
380
|
+
return JSON.parse(raw);
|
|
381
|
+
} catch {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/generators/askDrivenAsset.ts
|
|
387
|
+
var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
|
|
388
|
+
var FORMAT_GUIDES = {
|
|
389
|
+
markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
|
|
390
|
+
blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
|
|
391
|
+
social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
|
|
392
|
+
email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
|
|
393
|
+
slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
|
|
394
|
+
slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
|
|
395
|
+
};
|
|
396
|
+
function buildSystemPrompt6(format, tone) {
|
|
397
|
+
const toneMap = {
|
|
398
|
+
professional: "You are a professional technical writer and developer advocate.",
|
|
399
|
+
casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
|
|
400
|
+
technical: "You are a senior software engineer writing precise, implementation-focused content.",
|
|
401
|
+
executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
|
|
402
|
+
};
|
|
403
|
+
const guide = FORMAT_GUIDES[format];
|
|
404
|
+
return `${toneMap[tone] ?? toneMap.professional} Generate a ${guide.name} based on the user's question and the provided codebase architecture context. ${guide.structure} Write only the requested content \u2014 no preamble, no meta-commentary.`;
|
|
405
|
+
}
|
|
406
|
+
function buildUserPrompt6(blueprint, question, opts) {
|
|
407
|
+
const format = opts.format ?? "markdown";
|
|
408
|
+
const tone = opts.tone ?? "professional";
|
|
409
|
+
const length = opts.length ?? "medium";
|
|
410
|
+
const guide = FORMAT_GUIDES[format];
|
|
411
|
+
const lengthGuide = {
|
|
412
|
+
short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
|
|
413
|
+
medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
|
|
414
|
+
long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
|
|
415
|
+
}[length];
|
|
416
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
417
|
+
const lines = [
|
|
418
|
+
`## User's Question`,
|
|
419
|
+
question,
|
|
420
|
+
``,
|
|
421
|
+
`## Output Requirements`,
|
|
422
|
+
`- Format: ${guide.name}`,
|
|
423
|
+
`- Tone: ${tone}`,
|
|
424
|
+
`- Length: ${length} (${lengthGuide})`,
|
|
425
|
+
``,
|
|
426
|
+
`## Codebase Architecture Context`,
|
|
427
|
+
`Target: ${blueprint.targetPath}`,
|
|
428
|
+
`Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
429
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
430
|
+
``,
|
|
431
|
+
`Key files (by usage):`,
|
|
432
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
|
|
433
|
+
];
|
|
434
|
+
if (blueprint.edges.length > 0) {
|
|
435
|
+
const edgeSample = blueprint.edges.slice(0, 15);
|
|
436
|
+
lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
|
|
437
|
+
}
|
|
438
|
+
lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
|
|
439
|
+
return lines.join("\n");
|
|
440
|
+
}
|
|
441
|
+
async function generateAskDrivenAsset(blueprint, question, opts, provider) {
|
|
442
|
+
if (!blueprint) {
|
|
443
|
+
return {
|
|
444
|
+
text: NO_DATA_MSG5,
|
|
445
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (!question || !question.trim()) {
|
|
449
|
+
return {
|
|
450
|
+
text: "No question provided. Please ask something about your codebase.",
|
|
451
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const format = opts.format ?? "markdown";
|
|
455
|
+
const tone = opts.tone ?? "professional";
|
|
456
|
+
const maxTokens = FORMAT_GUIDES[format].maxTokens;
|
|
457
|
+
const response = await provider.complete({
|
|
458
|
+
systemPrompt: buildSystemPrompt6(format, tone),
|
|
459
|
+
messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
|
|
460
|
+
maxTokens
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
text: response.content,
|
|
464
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
465
|
+
};
|
|
466
|
+
}
|
|
103
467
|
|
|
468
|
+
exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
|
|
469
|
+
exports.generateAskDrivenAsset = generateAskDrivenAsset;
|
|
470
|
+
exports.generateChangesSince = generateChangesSince;
|
|
471
|
+
exports.generateOnboardingDoc = generateOnboardingDoc;
|
|
472
|
+
exports.generateRefactoringReport = generateRefactoringReport;
|
|
104
473
|
exports.generateReleaseNotes = generateReleaseNotes;
|
|
474
|
+
exports.readBlueprintData = readBlueprintData;
|
|
105
475
|
exports.readPrismDirectory = readPrismDirectory;
|
package/dist/index.d.cts
CHANGED
|
@@ -29,6 +29,62 @@ interface ReleaseNotesOpts {
|
|
|
29
29
|
length?: "short" | "medium" | "long";
|
|
30
30
|
format?: "markdown" | "plain";
|
|
31
31
|
}
|
|
32
|
+
interface BlueprintFile {
|
|
33
|
+
path: string;
|
|
34
|
+
category?: "app" | "component" | "lib" | "hook" | string;
|
|
35
|
+
importCount?: number;
|
|
36
|
+
importedByCount?: number;
|
|
37
|
+
lineCount?: number;
|
|
38
|
+
}
|
|
39
|
+
interface BlueprintEdge {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
}
|
|
44
|
+
interface BlueprintData {
|
|
45
|
+
targetPath: string;
|
|
46
|
+
scanTimestamp: number;
|
|
47
|
+
stats: {
|
|
48
|
+
totalFiles: number;
|
|
49
|
+
runtimeEdges: number;
|
|
50
|
+
[key: string]: number;
|
|
51
|
+
};
|
|
52
|
+
files: BlueprintFile[];
|
|
53
|
+
edges: BlueprintEdge[];
|
|
54
|
+
categories: {
|
|
55
|
+
app: number;
|
|
56
|
+
component: number;
|
|
57
|
+
lib: number;
|
|
58
|
+
hook: number;
|
|
59
|
+
[key: string]: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
interface ArchitectureWalkthroughOpts {
|
|
63
|
+
tone?: "technical" | "casual" | "onboarding";
|
|
64
|
+
length?: "short" | "medium" | "long";
|
|
65
|
+
format?: "markdown" | "plain";
|
|
66
|
+
}
|
|
67
|
+
interface ChangesSinceOpts {
|
|
68
|
+
tone?: "professional" | "casual" | "technical";
|
|
69
|
+
length?: "short" | "medium" | "long";
|
|
70
|
+
format?: "markdown" | "plain";
|
|
71
|
+
}
|
|
72
|
+
interface OnboardingDocOpts {
|
|
73
|
+
tone?: "friendly" | "formal" | "technical";
|
|
74
|
+
length?: "short" | "medium" | "long";
|
|
75
|
+
format?: "markdown" | "plain";
|
|
76
|
+
}
|
|
77
|
+
interface RefactoringReportOpts {
|
|
78
|
+
tone?: "analytical" | "casual" | "executive";
|
|
79
|
+
length?: "short" | "medium" | "long";
|
|
80
|
+
format?: "markdown" | "plain";
|
|
81
|
+
}
|
|
82
|
+
type AskDrivenAssetFormat = "markdown" | "blog" | "social" | "email" | "slack" | "slide";
|
|
83
|
+
interface AskDrivenAssetOpts {
|
|
84
|
+
format?: AskDrivenAssetFormat;
|
|
85
|
+
tone?: "professional" | "casual" | "technical" | "executive";
|
|
86
|
+
length?: "short" | "medium" | "long";
|
|
87
|
+
}
|
|
32
88
|
interface GenerationResult {
|
|
33
89
|
text: string;
|
|
34
90
|
metadata: {
|
|
@@ -58,6 +114,17 @@ interface LlmProvider {
|
|
|
58
114
|
|
|
59
115
|
declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
60
116
|
|
|
117
|
+
declare function generateArchitectureWalkthrough(blueprint: BlueprintData | null, opts: ArchitectureWalkthroughOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
118
|
+
|
|
119
|
+
declare function generateChangesSince(current: BlueprintData | null, previous: BlueprintData | null, opts: ChangesSinceOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
120
|
+
|
|
121
|
+
declare function generateOnboardingDoc(blueprint: BlueprintData | null, opts: OnboardingDocOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
122
|
+
|
|
123
|
+
declare function generateRefactoringReport(blueprint: BlueprintData | null, opts: RefactoringReportOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
124
|
+
|
|
61
125
|
declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
|
|
126
|
+
declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
|
|
127
|
+
|
|
128
|
+
declare function generateAskDrivenAsset(blueprint: BlueprintData | null, question: string, opts: AskDrivenAssetOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
62
129
|
|
|
63
|
-
export { type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type ReleaseNotesOpts, generateReleaseNotes, readPrismDirectory };
|
|
130
|
+
export { type ArchitectureWalkthroughOpts, type AskDrivenAssetFormat, type AskDrivenAssetOpts, type BlueprintData, type BlueprintEdge, type BlueprintFile, type ChangesSinceOpts, type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type OnboardingDocOpts, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type RefactoringReportOpts, type ReleaseNotesOpts, generateArchitectureWalkthrough, generateAskDrivenAsset, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,62 @@ interface ReleaseNotesOpts {
|
|
|
29
29
|
length?: "short" | "medium" | "long";
|
|
30
30
|
format?: "markdown" | "plain";
|
|
31
31
|
}
|
|
32
|
+
interface BlueprintFile {
|
|
33
|
+
path: string;
|
|
34
|
+
category?: "app" | "component" | "lib" | "hook" | string;
|
|
35
|
+
importCount?: number;
|
|
36
|
+
importedByCount?: number;
|
|
37
|
+
lineCount?: number;
|
|
38
|
+
}
|
|
39
|
+
interface BlueprintEdge {
|
|
40
|
+
from: string;
|
|
41
|
+
to: string;
|
|
42
|
+
type?: string;
|
|
43
|
+
}
|
|
44
|
+
interface BlueprintData {
|
|
45
|
+
targetPath: string;
|
|
46
|
+
scanTimestamp: number;
|
|
47
|
+
stats: {
|
|
48
|
+
totalFiles: number;
|
|
49
|
+
runtimeEdges: number;
|
|
50
|
+
[key: string]: number;
|
|
51
|
+
};
|
|
52
|
+
files: BlueprintFile[];
|
|
53
|
+
edges: BlueprintEdge[];
|
|
54
|
+
categories: {
|
|
55
|
+
app: number;
|
|
56
|
+
component: number;
|
|
57
|
+
lib: number;
|
|
58
|
+
hook: number;
|
|
59
|
+
[key: string]: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
interface ArchitectureWalkthroughOpts {
|
|
63
|
+
tone?: "technical" | "casual" | "onboarding";
|
|
64
|
+
length?: "short" | "medium" | "long";
|
|
65
|
+
format?: "markdown" | "plain";
|
|
66
|
+
}
|
|
67
|
+
interface ChangesSinceOpts {
|
|
68
|
+
tone?: "professional" | "casual" | "technical";
|
|
69
|
+
length?: "short" | "medium" | "long";
|
|
70
|
+
format?: "markdown" | "plain";
|
|
71
|
+
}
|
|
72
|
+
interface OnboardingDocOpts {
|
|
73
|
+
tone?: "friendly" | "formal" | "technical";
|
|
74
|
+
length?: "short" | "medium" | "long";
|
|
75
|
+
format?: "markdown" | "plain";
|
|
76
|
+
}
|
|
77
|
+
interface RefactoringReportOpts {
|
|
78
|
+
tone?: "analytical" | "casual" | "executive";
|
|
79
|
+
length?: "short" | "medium" | "long";
|
|
80
|
+
format?: "markdown" | "plain";
|
|
81
|
+
}
|
|
82
|
+
type AskDrivenAssetFormat = "markdown" | "blog" | "social" | "email" | "slack" | "slide";
|
|
83
|
+
interface AskDrivenAssetOpts {
|
|
84
|
+
format?: AskDrivenAssetFormat;
|
|
85
|
+
tone?: "professional" | "casual" | "technical" | "executive";
|
|
86
|
+
length?: "short" | "medium" | "long";
|
|
87
|
+
}
|
|
32
88
|
interface GenerationResult {
|
|
33
89
|
text: string;
|
|
34
90
|
metadata: {
|
|
@@ -58,6 +114,17 @@ interface LlmProvider {
|
|
|
58
114
|
|
|
59
115
|
declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
60
116
|
|
|
117
|
+
declare function generateArchitectureWalkthrough(blueprint: BlueprintData | null, opts: ArchitectureWalkthroughOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
118
|
+
|
|
119
|
+
declare function generateChangesSince(current: BlueprintData | null, previous: BlueprintData | null, opts: ChangesSinceOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
120
|
+
|
|
121
|
+
declare function generateOnboardingDoc(blueprint: BlueprintData | null, opts: OnboardingDocOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
122
|
+
|
|
123
|
+
declare function generateRefactoringReport(blueprint: BlueprintData | null, opts: RefactoringReportOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
124
|
+
|
|
61
125
|
declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
|
|
126
|
+
declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
|
|
127
|
+
|
|
128
|
+
declare function generateAskDrivenAsset(blueprint: BlueprintData | null, question: string, opts: AskDrivenAssetOpts, provider: LlmProvider): Promise<GenerationResult>;
|
|
62
129
|
|
|
63
|
-
export { type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type ReleaseNotesOpts, generateReleaseNotes, readPrismDirectory };
|
|
130
|
+
export { type ArchitectureWalkthroughOpts, type AskDrivenAssetFormat, type AskDrivenAssetOpts, type BlueprintData, type BlueprintEdge, type BlueprintFile, type ChangesSinceOpts, type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type OnboardingDocOpts, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type RefactoringReportOpts, type ReleaseNotesOpts, generateArchitectureWalkthrough, generateAskDrivenAsset, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
|
package/dist/index.mjs
CHANGED
|
@@ -65,6 +65,279 @@ async function generateReleaseNotes(data, opts, provider) {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// src/generators/architectureWalkthrough.ts
|
|
70
|
+
var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
|
|
71
|
+
function buildSystemPrompt2(tone) {
|
|
72
|
+
const toneMap = {
|
|
73
|
+
technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
|
|
74
|
+
casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
|
|
75
|
+
onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
|
|
76
|
+
};
|
|
77
|
+
return `${toneMap[tone] ?? toneMap.technical} Write only the requested document \u2014 no preamble, no meta-commentary.`;
|
|
78
|
+
}
|
|
79
|
+
function buildUserPrompt2(blueprint, opts) {
|
|
80
|
+
const tone = opts.tone ?? "technical";
|
|
81
|
+
const length = opts.length ?? "long";
|
|
82
|
+
const format = opts.format ?? "markdown";
|
|
83
|
+
const lengthGuide = {
|
|
84
|
+
short: "3-4 sections, concise overview",
|
|
85
|
+
medium: "5-7 sections with moderate detail",
|
|
86
|
+
long: "comprehensive, 8+ sections with full detail and examples"
|
|
87
|
+
}[length];
|
|
88
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 15);
|
|
89
|
+
const edgeSample = blueprint.edges.slice(0, 20);
|
|
90
|
+
const lines = [
|
|
91
|
+
`Generate an architecture walkthrough document in ${format} format for the codebase at "${blueprint.targetPath}".`,
|
|
92
|
+
`Tone: ${tone}. Length: ${length} (${lengthGuide}).`,
|
|
93
|
+
``,
|
|
94
|
+
`## Codebase Stats`,
|
|
95
|
+
`- Total files: ${blueprint.stats.totalFiles}`,
|
|
96
|
+
`- Runtime dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
97
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
98
|
+
``,
|
|
99
|
+
`## Key Files (by incoming dependency count)`,
|
|
100
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, imports: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
101
|
+
``,
|
|
102
|
+
`## Dependency Edge Sample`,
|
|
103
|
+
...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`),
|
|
104
|
+
``,
|
|
105
|
+
`Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
|
|
106
|
+
];
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
110
|
+
if (!blueprint) {
|
|
111
|
+
return {
|
|
112
|
+
text: NO_DATA_MSG,
|
|
113
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const response = await provider.complete({
|
|
117
|
+
systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
|
|
118
|
+
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
|
|
119
|
+
maxTokens: 4096
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
text: response.content,
|
|
123
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/generators/changesSince.ts
|
|
128
|
+
var NO_DATA_MSG2 = "No Blueprint data available. Run prism scan first.";
|
|
129
|
+
function buildSystemPrompt3() {
|
|
130
|
+
return `You are a technical writer who produces clear, concise architecture change summaries for development teams. Focus on what changed and why it matters. Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
131
|
+
}
|
|
132
|
+
function computeDelta(current, previous) {
|
|
133
|
+
const currentPaths = new Set(current.files.map((f) => f.path));
|
|
134
|
+
const previousPaths = new Set(previous.files.map((f) => f.path));
|
|
135
|
+
const added = current.files.filter((f) => !previousPaths.has(f.path));
|
|
136
|
+
const removed = previous.files.filter((f) => !currentPaths.has(f.path));
|
|
137
|
+
const currentEdgeKeys = new Set(current.edges.map((e) => `${e.from}\u2192${e.to}`));
|
|
138
|
+
const previousEdgeKeys = new Set(previous.edges.map((e) => `${e.from}\u2192${e.to}`));
|
|
139
|
+
const addedEdges = current.edges.filter((e) => !previousEdgeKeys.has(`${e.from}\u2192${e.to}`));
|
|
140
|
+
const removedEdges = previous.edges.filter((e) => !currentEdgeKeys.has(`${e.from}\u2192${e.to}`));
|
|
141
|
+
return { added, removed, addedEdges, removedEdges };
|
|
142
|
+
}
|
|
143
|
+
function buildUserPrompt3(current, previous, opts) {
|
|
144
|
+
const tone = opts.tone ?? "professional";
|
|
145
|
+
const length = opts.length ?? "medium";
|
|
146
|
+
const format = opts.format ?? "markdown";
|
|
147
|
+
const delta = computeDelta(current, previous);
|
|
148
|
+
const prevDate = new Date(previous.scanTimestamp).toISOString().split("T")[0];
|
|
149
|
+
const currDate = new Date(current.scanTimestamp).toISOString().split("T")[0];
|
|
150
|
+
const lines = [
|
|
151
|
+
`Generate an architecture change summary in ${format} format.`,
|
|
152
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
153
|
+
`Period: ${prevDate} \u2192 ${currDate}`,
|
|
154
|
+
``,
|
|
155
|
+
`## File Changes`,
|
|
156
|
+
`- Added files (${delta.added.length}): ${delta.added.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
|
|
157
|
+
`- Removed files (${delta.removed.length}): ${delta.removed.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
|
|
158
|
+
``,
|
|
159
|
+
`## Dependency Edge Changes`,
|
|
160
|
+
`- New edges (${delta.addedEdges.length}): ${delta.addedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
|
|
161
|
+
`- Removed edges (${delta.removedEdges.length}): ${delta.removedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
|
|
162
|
+
``,
|
|
163
|
+
`## Stats Delta`,
|
|
164
|
+
`- Files: ${previous.stats.totalFiles} \u2192 ${current.stats.totalFiles} (${current.stats.totalFiles - previous.stats.totalFiles >= 0 ? "+" : ""}${current.stats.totalFiles - previous.stats.totalFiles})`,
|
|
165
|
+
`- Edges: ${previous.stats.runtimeEdges} \u2192 ${current.stats.runtimeEdges} (${current.stats.runtimeEdges - previous.stats.runtimeEdges >= 0 ? "+" : ""}${current.stats.runtimeEdges - previous.stats.runtimeEdges})`,
|
|
166
|
+
``,
|
|
167
|
+
`Summarize what changed architecturally, highlight significant additions or removals, and note any structural shifts.`
|
|
168
|
+
];
|
|
169
|
+
return lines.join("\n");
|
|
170
|
+
}
|
|
171
|
+
async function generateChangesSince(current, previous, opts, provider) {
|
|
172
|
+
if (!current || !previous) {
|
|
173
|
+
return {
|
|
174
|
+
text: NO_DATA_MSG2,
|
|
175
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const response = await provider.complete({
|
|
179
|
+
systemPrompt: buildSystemPrompt3(),
|
|
180
|
+
messages: [{ role: "user", content: buildUserPrompt3(current, previous, opts) }],
|
|
181
|
+
maxTokens: 2048
|
|
182
|
+
});
|
|
183
|
+
return {
|
|
184
|
+
text: response.content,
|
|
185
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/generators/onboardingDoc.ts
|
|
190
|
+
var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
|
|
191
|
+
function buildSystemPrompt4(tone) {
|
|
192
|
+
const toneMap = {
|
|
193
|
+
friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
194
|
+
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams.",
|
|
195
|
+
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
|
|
196
|
+
};
|
|
197
|
+
return `${toneMap[tone] ?? toneMap.friendly} Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
198
|
+
}
|
|
199
|
+
function buildUserPrompt4(blueprint, opts) {
|
|
200
|
+
const tone = opts.tone ?? "friendly";
|
|
201
|
+
const length = opts.length ?? "medium";
|
|
202
|
+
const format = opts.format ?? "markdown";
|
|
203
|
+
const topEntryPoints = blueprint.files.filter((f) => f.category === "app" || (f.importedByCount ?? 0) === 0).slice(0, 10);
|
|
204
|
+
const topImported = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
|
|
205
|
+
const folderMap = /* @__PURE__ */ new Map();
|
|
206
|
+
for (const f of blueprint.files) {
|
|
207
|
+
const parts = f.path.split("/");
|
|
208
|
+
if (parts.length > 1) {
|
|
209
|
+
const folder = parts[0];
|
|
210
|
+
folderMap.set(folder, (folderMap.get(folder) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const topFolders = [...folderMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([folder, count]) => `${folder}/ (${count} files)`);
|
|
214
|
+
const lines = [
|
|
215
|
+
`Generate an onboarding document in ${format} format for new developers joining this codebase.`,
|
|
216
|
+
`Project path: "${blueprint.targetPath}"`,
|
|
217
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
218
|
+
``,
|
|
219
|
+
`## Codebase Overview`,
|
|
220
|
+
`- ${blueprint.stats.totalFiles} files across ${Object.keys(blueprint.categories).length} categories`,
|
|
221
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
222
|
+
``,
|
|
223
|
+
`## Top-level Folders`,
|
|
224
|
+
...topFolders.map((f) => `- ${f}`),
|
|
225
|
+
``,
|
|
226
|
+
`## Entry Points (app-layer files, zero incoming deps)`,
|
|
227
|
+
...topEntryPoints.map((f) => `- ${f.path}`),
|
|
228
|
+
``,
|
|
229
|
+
`## Core Shared Files (most imported)`,
|
|
230
|
+
...topImported.map((f) => `- ${f.path} \u2014 used by ${f.importedByCount ?? 0} files`),
|
|
231
|
+
``,
|
|
232
|
+
`Include: "Start here" section, key folders tour, important conventions to follow, the 3-5 files to read first, and how to run/test the project.`
|
|
233
|
+
];
|
|
234
|
+
return lines.join("\n");
|
|
235
|
+
}
|
|
236
|
+
async function generateOnboardingDoc(blueprint, opts, provider) {
|
|
237
|
+
if (!blueprint) {
|
|
238
|
+
return {
|
|
239
|
+
text: NO_DATA_MSG3,
|
|
240
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const response = await provider.complete({
|
|
244
|
+
systemPrompt: buildSystemPrompt4(opts.tone ?? "friendly"),
|
|
245
|
+
messages: [{ role: "user", content: buildUserPrompt4(blueprint, opts) }],
|
|
246
|
+
maxTokens: 3072
|
|
247
|
+
});
|
|
248
|
+
return {
|
|
249
|
+
text: response.content,
|
|
250
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/generators/refactoringReport.ts
|
|
255
|
+
var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
|
|
256
|
+
function buildSystemPrompt5(tone) {
|
|
257
|
+
const toneMap = {
|
|
258
|
+
analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
|
|
259
|
+
casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
|
|
260
|
+
executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership."
|
|
261
|
+
};
|
|
262
|
+
return `${toneMap[tone] ?? toneMap.analytical} Write only the report \u2014 no preamble, no meta-commentary.`;
|
|
263
|
+
}
|
|
264
|
+
function detectImportCycles(edges) {
|
|
265
|
+
const graph = /* @__PURE__ */ new Map();
|
|
266
|
+
for (const e of edges) {
|
|
267
|
+
if (!graph.has(e.from)) graph.set(e.from, /* @__PURE__ */ new Set());
|
|
268
|
+
graph.get(e.from).add(e.to);
|
|
269
|
+
}
|
|
270
|
+
const cycles = [];
|
|
271
|
+
const visited = /* @__PURE__ */ new Set();
|
|
272
|
+
const stack = /* @__PURE__ */ new Set();
|
|
273
|
+
function dfs(node, path2) {
|
|
274
|
+
if (cycles.length >= 5) return;
|
|
275
|
+
if (stack.has(node)) {
|
|
276
|
+
const cycleStart = path2.indexOf(node);
|
|
277
|
+
if (cycleStart !== -1) {
|
|
278
|
+
cycles.push(path2.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (visited.has(node)) return;
|
|
283
|
+
visited.add(node);
|
|
284
|
+
stack.add(node);
|
|
285
|
+
for (const neighbor of graph.get(node) ?? []) {
|
|
286
|
+
dfs(neighbor, [...path2, node]);
|
|
287
|
+
}
|
|
288
|
+
stack.delete(node);
|
|
289
|
+
}
|
|
290
|
+
for (const node of graph.keys()) {
|
|
291
|
+
if (!visited.has(node)) dfs(node, []);
|
|
292
|
+
}
|
|
293
|
+
return cycles;
|
|
294
|
+
}
|
|
295
|
+
function buildUserPrompt5(blueprint, opts) {
|
|
296
|
+
const tone = opts.tone ?? "analytical";
|
|
297
|
+
const length = opts.length ?? "medium";
|
|
298
|
+
const format = opts.format ?? "markdown";
|
|
299
|
+
const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
|
|
300
|
+
const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
|
|
301
|
+
const importCycles = detectImportCycles(blueprint.edges);
|
|
302
|
+
const lines = [
|
|
303
|
+
`Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
|
|
304
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
305
|
+
``,
|
|
306
|
+
`## Architecture Metrics`,
|
|
307
|
+
`- Total files: ${blueprint.stats.totalFiles}`,
|
|
308
|
+
`- Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
309
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
310
|
+
``,
|
|
311
|
+
`## High-Coupling Hot Spots (high total connections)`,
|
|
312
|
+
...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
313
|
+
``,
|
|
314
|
+
`## High Fan-Out Files (many outgoing dependencies)`,
|
|
315
|
+
...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
|
|
316
|
+
``,
|
|
317
|
+
`## Import Cycles Detected (${importCycles.length})`,
|
|
318
|
+
...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
|
|
319
|
+
``,
|
|
320
|
+
`Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
|
|
321
|
+
];
|
|
322
|
+
return lines.join("\n");
|
|
323
|
+
}
|
|
324
|
+
async function generateRefactoringReport(blueprint, opts, provider) {
|
|
325
|
+
if (!blueprint) {
|
|
326
|
+
return {
|
|
327
|
+
text: NO_DATA_MSG4,
|
|
328
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
const response = await provider.complete({
|
|
332
|
+
systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
|
|
333
|
+
messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts) }],
|
|
334
|
+
maxTokens: 3072
|
|
335
|
+
});
|
|
336
|
+
return {
|
|
337
|
+
text: response.content,
|
|
338
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
339
|
+
};
|
|
340
|
+
}
|
|
68
341
|
async function readJsonFiles(dir) {
|
|
69
342
|
try {
|
|
70
343
|
const entries = await fs.readdir(dir);
|
|
@@ -93,5 +366,96 @@ async function readPrismDirectory(prismPath) {
|
|
|
93
366
|
]);
|
|
94
367
|
return { sessions, recommendations, insights };
|
|
95
368
|
}
|
|
369
|
+
async function readBlueprintData(targetPath) {
|
|
370
|
+
const snapshotPath = path.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
371
|
+
try {
|
|
372
|
+
const raw = await fs.readFile(snapshotPath, "utf-8");
|
|
373
|
+
return JSON.parse(raw);
|
|
374
|
+
} catch {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/generators/askDrivenAsset.ts
|
|
380
|
+
var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
|
|
381
|
+
var FORMAT_GUIDES = {
|
|
382
|
+
markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
|
|
383
|
+
blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
|
|
384
|
+
social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
|
|
385
|
+
email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
|
|
386
|
+
slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
|
|
387
|
+
slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
|
|
388
|
+
};
|
|
389
|
+
function buildSystemPrompt6(format, tone) {
|
|
390
|
+
const toneMap = {
|
|
391
|
+
professional: "You are a professional technical writer and developer advocate.",
|
|
392
|
+
casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
|
|
393
|
+
technical: "You are a senior software engineer writing precise, implementation-focused content.",
|
|
394
|
+
executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
|
|
395
|
+
};
|
|
396
|
+
const guide = FORMAT_GUIDES[format];
|
|
397
|
+
return `${toneMap[tone] ?? toneMap.professional} Generate a ${guide.name} based on the user's question and the provided codebase architecture context. ${guide.structure} Write only the requested content \u2014 no preamble, no meta-commentary.`;
|
|
398
|
+
}
|
|
399
|
+
function buildUserPrompt6(blueprint, question, opts) {
|
|
400
|
+
const format = opts.format ?? "markdown";
|
|
401
|
+
const tone = opts.tone ?? "professional";
|
|
402
|
+
const length = opts.length ?? "medium";
|
|
403
|
+
const guide = FORMAT_GUIDES[format];
|
|
404
|
+
const lengthGuide = {
|
|
405
|
+
short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
|
|
406
|
+
medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
|
|
407
|
+
long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
|
|
408
|
+
}[length];
|
|
409
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
410
|
+
const lines = [
|
|
411
|
+
`## User's Question`,
|
|
412
|
+
question,
|
|
413
|
+
``,
|
|
414
|
+
`## Output Requirements`,
|
|
415
|
+
`- Format: ${guide.name}`,
|
|
416
|
+
`- Tone: ${tone}`,
|
|
417
|
+
`- Length: ${length} (${lengthGuide})`,
|
|
418
|
+
``,
|
|
419
|
+
`## Codebase Architecture Context`,
|
|
420
|
+
`Target: ${blueprint.targetPath}`,
|
|
421
|
+
`Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
422
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
423
|
+
``,
|
|
424
|
+
`Key files (by usage):`,
|
|
425
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
|
|
426
|
+
];
|
|
427
|
+
if (blueprint.edges.length > 0) {
|
|
428
|
+
const edgeSample = blueprint.edges.slice(0, 15);
|
|
429
|
+
lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
|
|
430
|
+
}
|
|
431
|
+
lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
|
|
432
|
+
return lines.join("\n");
|
|
433
|
+
}
|
|
434
|
+
async function generateAskDrivenAsset(blueprint, question, opts, provider) {
|
|
435
|
+
if (!blueprint) {
|
|
436
|
+
return {
|
|
437
|
+
text: NO_DATA_MSG5,
|
|
438
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
if (!question || !question.trim()) {
|
|
442
|
+
return {
|
|
443
|
+
text: "No question provided. Please ask something about your codebase.",
|
|
444
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
const format = opts.format ?? "markdown";
|
|
448
|
+
const tone = opts.tone ?? "professional";
|
|
449
|
+
const maxTokens = FORMAT_GUIDES[format].maxTokens;
|
|
450
|
+
const response = await provider.complete({
|
|
451
|
+
systemPrompt: buildSystemPrompt6(format, tone),
|
|
452
|
+
messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
|
|
453
|
+
maxTokens
|
|
454
|
+
});
|
|
455
|
+
return {
|
|
456
|
+
text: response.content,
|
|
457
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
458
|
+
};
|
|
459
|
+
}
|
|
96
460
|
|
|
97
|
-
export { generateReleaseNotes, readPrismDirectory };
|
|
461
|
+
export { generateArchitectureWalkthrough, generateAskDrivenAsset, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
|
package/package.json
CHANGED