forgesmith 0.1.0 → 0.2.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — Architecture Narrative Generators
4
+
5
+ - `generateArchitectureWalkthrough(blueprint, opts, provider)` — long-form narrative explaining structure, key files, and important relationships. Tones: `technical` | `casual` | `onboarding`
6
+ - `generateChangesSince(current, previous, opts, provider)` — delta narrative: what changed between two Blueprint snapshots (new/removed files, edge changes, stats delta)
7
+ - `generateOnboardingDoc(blueprint, opts, provider)` — new-developer intro to codebase with "Start here", key folders, core shared files. Tones: `friendly` | `formal` | `technical`
8
+ - `generateRefactoringReport(blueprint, opts, provider)` — identifies architecture debt (hot spots, high fan-out, import cycles), suggests refactoring priorities. Tones: `analytical` | `casual` | `executive`
9
+ - `readBlueprintData(targetPath)` — reads `.prism/blueprint/snapshot.json`; returns `BlueprintData | null`
10
+ - New exported types: `BlueprintData`, `BlueprintFile`, `BlueprintEdge`, `ArchitectureWalkthroughOpts`, `ChangesSinceOpts`, `OnboardingDocOpts`, `RefactoringReportOpts`
11
+ - 44 unit tests, all green (+31 new)
12
+
13
+ ## 0.1.1 — /providers sub-entry fix
14
+
15
+ - Add explicit `./providers` export entry to package.json exports map
16
+ - Add `src/providers.ts` barrel (re-exports `DirectLlmProvider`)
17
+ - Fixes `Module not found: Can't resolve 'forgesmith/providers'` for consumers
18
+
3
19
  ## 0.1.0 — Initial real release
4
20
 
5
21
  - `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,20 @@ 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
+ }
103
385
 
386
+ exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
387
+ exports.generateChangesSince = generateChangesSince;
388
+ exports.generateOnboardingDoc = generateOnboardingDoc;
389
+ exports.generateRefactoringReport = generateRefactoringReport;
104
390
  exports.generateReleaseNotes = generateReleaseNotes;
391
+ exports.readBlueprintData = readBlueprintData;
105
392
  exports.readPrismDirectory = readPrismDirectory;
package/dist/index.d.cts CHANGED
@@ -29,6 +29,56 @@ 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
+ }
32
82
  interface GenerationResult {
33
83
  text: string;
34
84
  metadata: {
@@ -58,6 +108,15 @@ interface LlmProvider {
58
108
 
59
109
  declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
60
110
 
111
+ declare function generateArchitectureWalkthrough(blueprint: BlueprintData | null, opts: ArchitectureWalkthroughOpts, provider: LlmProvider): Promise<GenerationResult>;
112
+
113
+ declare function generateChangesSince(current: BlueprintData | null, previous: BlueprintData | null, opts: ChangesSinceOpts, provider: LlmProvider): Promise<GenerationResult>;
114
+
115
+ declare function generateOnboardingDoc(blueprint: BlueprintData | null, opts: OnboardingDocOpts, provider: LlmProvider): Promise<GenerationResult>;
116
+
117
+ declare function generateRefactoringReport(blueprint: BlueprintData | null, opts: RefactoringReportOpts, provider: LlmProvider): Promise<GenerationResult>;
118
+
61
119
  declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
120
+ declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
62
121
 
63
- export { type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type ReleaseNotesOpts, generateReleaseNotes, readPrismDirectory };
122
+ export { type ArchitectureWalkthroughOpts, 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, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
package/dist/index.d.ts CHANGED
@@ -29,6 +29,56 @@ 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
+ }
32
82
  interface GenerationResult {
33
83
  text: string;
34
84
  metadata: {
@@ -58,6 +108,15 @@ interface LlmProvider {
58
108
 
59
109
  declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
60
110
 
111
+ declare function generateArchitectureWalkthrough(blueprint: BlueprintData | null, opts: ArchitectureWalkthroughOpts, provider: LlmProvider): Promise<GenerationResult>;
112
+
113
+ declare function generateChangesSince(current: BlueprintData | null, previous: BlueprintData | null, opts: ChangesSinceOpts, provider: LlmProvider): Promise<GenerationResult>;
114
+
115
+ declare function generateOnboardingDoc(blueprint: BlueprintData | null, opts: OnboardingDocOpts, provider: LlmProvider): Promise<GenerationResult>;
116
+
117
+ declare function generateRefactoringReport(blueprint: BlueprintData | null, opts: RefactoringReportOpts, provider: LlmProvider): Promise<GenerationResult>;
118
+
61
119
  declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
120
+ declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
62
121
 
63
- export { type GenerationResult, type LlmMessage, type LlmProvider, type LlmRequest, type LlmResponse, type PrismData, type PrismInsight, type PrismRecommendation, type PrismSession, type ReleaseNotesOpts, generateReleaseNotes, readPrismDirectory };
122
+ export { type ArchitectureWalkthroughOpts, 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, 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,14 @@ 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
+ }
96
378
 
97
- export { generateReleaseNotes, readPrismDirectory };
379
+ export { generateArchitectureWalkthrough, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgesmith",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "forgesmith — content & asset-generation engine. Forge release notes, blog posts, social copy from prism0x2A data.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",