forgesmith 0.0.1 → 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 ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
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
+
19
+ ## 0.1.0 — Initial real release
20
+
21
+ - `generateReleaseNotes(data, opts, provider)` — LLM-powered release note generation from prism0x2A data
22
+ - `readPrismDirectory(path)` — reads `.prism/` folder structure into `PrismData`
23
+ - `DirectLlmProvider` — Anthropic SDK-backed LLM provider (import from `forgesmith/providers`)
24
+ - `LlmProvider` interface for custom/mock providers
25
+ - ESM + CJS dual-build via tsup
26
+ - 13 unit tests, all green
package/README.md CHANGED
@@ -1,15 +1,53 @@
1
1
  # forgesmith
2
2
 
3
- > Content & asset-generation engine for code intelligence.
4
- >
5
- > *The lens observes. The smith forges.*
3
+ Content & asset-generation engine for code intelligence.
6
4
 
7
- **Status:** Reserved first release follows after prism0x2A v0.3.x ships.
5
+ Forge release notes, blog posts, and social copy from [prism0x2A](https://github.com/dadenjo/prism0x2A) data.
8
6
 
9
- `forgesmith` is the engine library behind [`forge0x2B`](https://github.com/dadenjo/forge0x2B).
10
- It transforms code intelligence from [`prismlens`](https://github.com/dadenjo/prismlens) /
11
- [`prism0x2A`](https://github.com/dadenjo/prism0x2A) into communication-ready content:
12
- release notes, blog posts, social copy, internal updates.
7
+ ## Status
8
+
9
+ **Phase 1** — release-notes generator. More asset-types coming in Phase 2.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install forgesmith
15
+ ```
16
+
17
+ ## Use
18
+
19
+ ```ts
20
+ import { generateReleaseNotes, readPrismDirectory } from 'forgesmith'
21
+ import { DirectLlmProvider } from 'forgesmith/providers'
22
+
23
+ const data = await readPrismDirectory('./my-project/.prism')
24
+ const provider = new DirectLlmProvider({ apiKey: process.env.ANTHROPIC_KEY })
25
+ const result = await generateReleaseNotes(data, { tone: 'professional' }, provider)
26
+ console.log(result.text)
27
+ ```
28
+
29
+ ## API
30
+
31
+ ### `generateReleaseNotes(data, opts, provider)`
32
+
33
+ Generates release notes from prism data.
34
+
35
+ - `data: PrismData` — sessions, recommendations, insights from prism0x2A
36
+ - `opts: ReleaseNotesOpts` — `tone`, `length`, `format`
37
+ - `provider: LlmProvider` — any compatible LLM provider
38
+
39
+ ### `readPrismDirectory(prismPath)`
40
+
41
+ Reads a `.prism/` folder and returns a `PrismData` object.
42
+
43
+ ### `DirectLlmProvider`
44
+
45
+ Anthropic SDK-backed provider. Import from `forgesmith/providers`.
46
+
47
+ ```ts
48
+ import { DirectLlmProvider } from 'forgesmith/providers'
49
+ const provider = new DirectLlmProvider({ apiKey: '...', model: 'claude-sonnet-4-6' })
50
+ ```
13
51
 
14
52
  ## Architecture
15
53
 
@@ -20,20 +58,10 @@ prism0x2A (dashboard — persists .prism/ data)
20
58
 
21
59
  forgesmith (this — forges assets from data)
22
60
 
23
- forge0x2B (dashboard product for marketing/comms)
61
+ forge0x2B (dashboard for marketing/comms)
24
62
  ```
25
63
 
26
- ## Naming
27
-
28
- - **prismlens** *observes* — splits code into spectral analysis
29
- - **forgesmith** *produces* — takes the analysis and hammers out content
30
-
31
- Engines in this family carry **craft-appropriate** suffixes, not a forced uniform suffix.
32
-
33
- ## Companion repos
64
+ ## Related
34
65
 
35
- | Repo | Role |
36
- |---|---|
37
- | [`prismlens`](https://github.com/dadenjo/prismlens) | Code-intelligence engine |
38
- | [`prism0x2A`](https://github.com/dadenjo/prism0x2A) | Code-intelligence dashboard |
39
- | [`forge0x2B`](https://github.com/dadenjo/forge0x2B) | Content/comms dashboard |
66
+ - [prism0x2A](https://www.npmjs.com/package/prism0x2a) the intelligence layer
67
+ - [forge0x2B](https://www.npmjs.com/package/forge0x2b) — the dashboard that uses this engine
package/dist/index.cjs ADDED
@@ -0,0 +1,392 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs/promises');
4
+ var path = require('path');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var fs__default = /*#__PURE__*/_interopDefault(fs);
9
+ var path__default = /*#__PURE__*/_interopDefault(path);
10
+
11
+ // src/generators/releaseNotes.ts
12
+ function buildSystemPrompt() {
13
+ return `You are a technical writer and developer-relations expert. You generate clear, accurate release notes from structured code-intelligence data. Write only the release notes \u2014 no preamble, no meta-commentary.`;
14
+ }
15
+ function buildUserPrompt(data, opts) {
16
+ const tone = opts.tone ?? "professional";
17
+ const length = opts.length ?? "medium";
18
+ const format = opts.format ?? "markdown";
19
+ const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
20
+ const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
21
+ const hasSessions = (data.sessions?.length ?? 0) > 0;
22
+ const hasRecs = (data.recommendations?.length ?? 0) > 0;
23
+ const hasInsights = (data.insights?.length ?? 0) > 0;
24
+ if (!hasSessions && !hasRecs && !hasInsights) {
25
+ return `Generate a brief release note in ${format} format stating there are no changes to report in this period${data.fromDate ? ` (${data.fromDate} to ${data.toDate ?? "now"})` : ""}.`;
26
+ }
27
+ const lines = [];
28
+ lines.push(`Generate release notes with the following requirements:`);
29
+ lines.push(`- Tone: ${tone} (${toneGuide})`);
30
+ lines.push(`- Length: ${length} (${lengthGuide})`);
31
+ lines.push(`- Format: ${format}`);
32
+ if (data.fromDate) lines.push(`- Period: ${data.fromDate}${data.toDate ? ` to ${data.toDate}` : " to now"}`);
33
+ lines.push(``);
34
+ if (hasSessions) {
35
+ lines.push(`## Sessions (${data.sessions.length})`);
36
+ for (const s of data.sessions) {
37
+ lines.push(`- **${s.title}**${s.summary ? `: ${s.summary}` : ""}${s.conclusion ? ` | Conclusion: ${s.conclusion}` : ""}`);
38
+ }
39
+ lines.push(``);
40
+ }
41
+ if (hasRecs) {
42
+ lines.push(`## Recommendations (${data.recommendations.length})`);
43
+ for (const r of data.recommendations) {
44
+ const accepted = r.accepted === true ? " [ACCEPTED]" : r.accepted === false ? " [DECLINED]" : "";
45
+ lines.push(`- [${r.severity?.toUpperCase() ?? "INFO"}${accepted}] **${r.title}**${r.description ? `: ${r.description}` : ""}`);
46
+ }
47
+ lines.push(``);
48
+ }
49
+ if (hasInsights) {
50
+ lines.push(`## Insights (${data.insights.length})`);
51
+ for (const i of data.insights) {
52
+ lines.push(`- **${i.title}**${i.body ? `: ${i.body}` : ""}`);
53
+ }
54
+ lines.push(``);
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+ async function generateReleaseNotes(data, opts, provider) {
59
+ const systemPrompt = buildSystemPrompt();
60
+ const userPrompt = buildUserPrompt(data, opts);
61
+ const response = await provider.complete({
62
+ systemPrompt,
63
+ messages: [{ role: "user", content: userPrompt }],
64
+ maxTokens: 2048
65
+ });
66
+ return {
67
+ text: response.content,
68
+ metadata: {
69
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
70
+ usedTokens: response.usedTokens,
71
+ generator: "forgesmith"
72
+ }
73
+ };
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
+ }
348
+ async function readJsonFiles(dir) {
349
+ try {
350
+ const entries = await fs__default.default.readdir(dir);
351
+ const results = [];
352
+ for (const entry of entries) {
353
+ if (!entry.endsWith(".json")) continue;
354
+ try {
355
+ const raw = await fs__default.default.readFile(path__default.default.join(dir, entry), "utf-8");
356
+ results.push(JSON.parse(raw));
357
+ } catch {
358
+ }
359
+ }
360
+ return results;
361
+ } catch {
362
+ return [];
363
+ }
364
+ }
365
+ async function readPrismDirectory(prismPath) {
366
+ const sessionsDir = path__default.default.join(prismPath, "sessions");
367
+ const recsDir = path__default.default.join(prismPath, "recommendations");
368
+ const insightsDir = path__default.default.join(prismPath, "green", "insights", "accepted");
369
+ const [sessions, recommendations, insights] = await Promise.all([
370
+ readJsonFiles(sessionsDir),
371
+ readJsonFiles(recsDir),
372
+ readJsonFiles(insightsDir)
373
+ ]);
374
+ return { sessions, recommendations, insights };
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
+ exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
387
+ exports.generateChangesSince = generateChangesSince;
388
+ exports.generateOnboardingDoc = generateOnboardingDoc;
389
+ exports.generateRefactoringReport = generateRefactoringReport;
390
+ exports.generateReleaseNotes = generateReleaseNotes;
391
+ exports.readBlueprintData = readBlueprintData;
392
+ exports.readPrismDirectory = readPrismDirectory;
@@ -0,0 +1,122 @@
1
+ interface PrismSession {
2
+ id: string;
3
+ title: string;
4
+ summary?: string;
5
+ conclusion?: string;
6
+ createdAt?: string;
7
+ }
8
+ interface PrismRecommendation {
9
+ id: string;
10
+ title: string;
11
+ severity?: "low" | "medium" | "high" | "critical";
12
+ accepted?: boolean;
13
+ description?: string;
14
+ }
15
+ interface PrismInsight {
16
+ id: string;
17
+ title: string;
18
+ body?: string;
19
+ }
20
+ interface PrismData {
21
+ sessions?: PrismSession[];
22
+ recommendations?: PrismRecommendation[];
23
+ insights?: PrismInsight[];
24
+ fromDate?: string;
25
+ toDate?: string;
26
+ }
27
+ interface ReleaseNotesOpts {
28
+ tone?: "professional" | "casual" | "technical";
29
+ length?: "short" | "medium" | "long";
30
+ format?: "markdown" | "plain";
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
+ interface GenerationResult {
83
+ text: string;
84
+ metadata: {
85
+ generatedAt: string;
86
+ usedTokens: number;
87
+ generator: "forgesmith";
88
+ };
89
+ }
90
+
91
+ interface LlmMessage {
92
+ role: "user" | "assistant";
93
+ content: string;
94
+ }
95
+ interface LlmRequest {
96
+ model?: string;
97
+ messages: LlmMessage[];
98
+ maxTokens?: number;
99
+ systemPrompt?: string;
100
+ }
101
+ interface LlmResponse {
102
+ content: string;
103
+ usedTokens: number;
104
+ }
105
+ interface LlmProvider {
106
+ complete(request: LlmRequest): Promise<LlmResponse>;
107
+ }
108
+
109
+ declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
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
+
119
+ declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
120
+ declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
121
+
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 };
@@ -0,0 +1,122 @@
1
+ interface PrismSession {
2
+ id: string;
3
+ title: string;
4
+ summary?: string;
5
+ conclusion?: string;
6
+ createdAt?: string;
7
+ }
8
+ interface PrismRecommendation {
9
+ id: string;
10
+ title: string;
11
+ severity?: "low" | "medium" | "high" | "critical";
12
+ accepted?: boolean;
13
+ description?: string;
14
+ }
15
+ interface PrismInsight {
16
+ id: string;
17
+ title: string;
18
+ body?: string;
19
+ }
20
+ interface PrismData {
21
+ sessions?: PrismSession[];
22
+ recommendations?: PrismRecommendation[];
23
+ insights?: PrismInsight[];
24
+ fromDate?: string;
25
+ toDate?: string;
26
+ }
27
+ interface ReleaseNotesOpts {
28
+ tone?: "professional" | "casual" | "technical";
29
+ length?: "short" | "medium" | "long";
30
+ format?: "markdown" | "plain";
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
+ interface GenerationResult {
83
+ text: string;
84
+ metadata: {
85
+ generatedAt: string;
86
+ usedTokens: number;
87
+ generator: "forgesmith";
88
+ };
89
+ }
90
+
91
+ interface LlmMessage {
92
+ role: "user" | "assistant";
93
+ content: string;
94
+ }
95
+ interface LlmRequest {
96
+ model?: string;
97
+ messages: LlmMessage[];
98
+ maxTokens?: number;
99
+ systemPrompt?: string;
100
+ }
101
+ interface LlmResponse {
102
+ content: string;
103
+ usedTokens: number;
104
+ }
105
+ interface LlmProvider {
106
+ complete(request: LlmRequest): Promise<LlmResponse>;
107
+ }
108
+
109
+ declare function generateReleaseNotes(data: PrismData, opts: ReleaseNotesOpts, provider: LlmProvider): Promise<GenerationResult>;
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
+
119
+ declare function readPrismDirectory(prismPath: string): Promise<PrismData>;
120
+ declare function readBlueprintData(targetPath: string): Promise<BlueprintData | null>;
121
+
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 ADDED
@@ -0,0 +1,379 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+
4
+ // src/generators/releaseNotes.ts
5
+ function buildSystemPrompt() {
6
+ return `You are a technical writer and developer-relations expert. You generate clear, accurate release notes from structured code-intelligence data. Write only the release notes \u2014 no preamble, no meta-commentary.`;
7
+ }
8
+ function buildUserPrompt(data, opts) {
9
+ const tone = opts.tone ?? "professional";
10
+ const length = opts.length ?? "medium";
11
+ const format = opts.format ?? "markdown";
12
+ const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
13
+ const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
14
+ const hasSessions = (data.sessions?.length ?? 0) > 0;
15
+ const hasRecs = (data.recommendations?.length ?? 0) > 0;
16
+ const hasInsights = (data.insights?.length ?? 0) > 0;
17
+ if (!hasSessions && !hasRecs && !hasInsights) {
18
+ return `Generate a brief release note in ${format} format stating there are no changes to report in this period${data.fromDate ? ` (${data.fromDate} to ${data.toDate ?? "now"})` : ""}.`;
19
+ }
20
+ const lines = [];
21
+ lines.push(`Generate release notes with the following requirements:`);
22
+ lines.push(`- Tone: ${tone} (${toneGuide})`);
23
+ lines.push(`- Length: ${length} (${lengthGuide})`);
24
+ lines.push(`- Format: ${format}`);
25
+ if (data.fromDate) lines.push(`- Period: ${data.fromDate}${data.toDate ? ` to ${data.toDate}` : " to now"}`);
26
+ lines.push(``);
27
+ if (hasSessions) {
28
+ lines.push(`## Sessions (${data.sessions.length})`);
29
+ for (const s of data.sessions) {
30
+ lines.push(`- **${s.title}**${s.summary ? `: ${s.summary}` : ""}${s.conclusion ? ` | Conclusion: ${s.conclusion}` : ""}`);
31
+ }
32
+ lines.push(``);
33
+ }
34
+ if (hasRecs) {
35
+ lines.push(`## Recommendations (${data.recommendations.length})`);
36
+ for (const r of data.recommendations) {
37
+ const accepted = r.accepted === true ? " [ACCEPTED]" : r.accepted === false ? " [DECLINED]" : "";
38
+ lines.push(`- [${r.severity?.toUpperCase() ?? "INFO"}${accepted}] **${r.title}**${r.description ? `: ${r.description}` : ""}`);
39
+ }
40
+ lines.push(``);
41
+ }
42
+ if (hasInsights) {
43
+ lines.push(`## Insights (${data.insights.length})`);
44
+ for (const i of data.insights) {
45
+ lines.push(`- **${i.title}**${i.body ? `: ${i.body}` : ""}`);
46
+ }
47
+ lines.push(``);
48
+ }
49
+ return lines.join("\n");
50
+ }
51
+ async function generateReleaseNotes(data, opts, provider) {
52
+ const systemPrompt = buildSystemPrompt();
53
+ const userPrompt = buildUserPrompt(data, opts);
54
+ const response = await provider.complete({
55
+ systemPrompt,
56
+ messages: [{ role: "user", content: userPrompt }],
57
+ maxTokens: 2048
58
+ });
59
+ return {
60
+ text: response.content,
61
+ metadata: {
62
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
63
+ usedTokens: response.usedTokens,
64
+ generator: "forgesmith"
65
+ }
66
+ };
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
+ }
341
+ async function readJsonFiles(dir) {
342
+ try {
343
+ const entries = await fs.readdir(dir);
344
+ const results = [];
345
+ for (const entry of entries) {
346
+ if (!entry.endsWith(".json")) continue;
347
+ try {
348
+ const raw = await fs.readFile(path.join(dir, entry), "utf-8");
349
+ results.push(JSON.parse(raw));
350
+ } catch {
351
+ }
352
+ }
353
+ return results;
354
+ } catch {
355
+ return [];
356
+ }
357
+ }
358
+ async function readPrismDirectory(prismPath) {
359
+ const sessionsDir = path.join(prismPath, "sessions");
360
+ const recsDir = path.join(prismPath, "recommendations");
361
+ const insightsDir = path.join(prismPath, "green", "insights", "accepted");
362
+ const [sessions, recommendations, insights] = await Promise.all([
363
+ readJsonFiles(sessionsDir),
364
+ readJsonFiles(recsDir),
365
+ readJsonFiles(insightsDir)
366
+ ]);
367
+ return { sessions, recommendations, insights };
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
+ export { generateArchitectureWalkthrough, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, readBlueprintData, readPrismDirectory };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ var Anthropic = require('@anthropic-ai/sdk');
4
+
5
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
6
+
7
+ var Anthropic__default = /*#__PURE__*/_interopDefault(Anthropic);
8
+
9
+ // src/llm/providers/direct.ts
10
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
11
+ var DirectLlmProvider = class {
12
+ client;
13
+ defaultModel;
14
+ constructor(opts) {
15
+ this.client = new Anthropic__default.default({ apiKey: opts.apiKey });
16
+ this.defaultModel = opts.model ?? DEFAULT_MODEL;
17
+ }
18
+ async complete(request) {
19
+ const model = request.model ?? this.defaultModel;
20
+ const maxTokens = request.maxTokens ?? 4096;
21
+ const response = await this.client.messages.create({
22
+ model,
23
+ max_tokens: maxTokens,
24
+ system: request.systemPrompt,
25
+ messages: request.messages.map((m) => ({
26
+ role: m.role,
27
+ content: m.content
28
+ }))
29
+ });
30
+ const content = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
31
+ const usedTokens = (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0);
32
+ return { content, usedTokens };
33
+ }
34
+ };
35
+
36
+ exports.DirectLlmProvider = DirectLlmProvider;
@@ -0,0 +1,30 @@
1
+ interface LlmMessage {
2
+ role: "user" | "assistant";
3
+ content: string;
4
+ }
5
+ interface LlmRequest {
6
+ model?: string;
7
+ messages: LlmMessage[];
8
+ maxTokens?: number;
9
+ systemPrompt?: string;
10
+ }
11
+ interface LlmResponse {
12
+ content: string;
13
+ usedTokens: number;
14
+ }
15
+ interface LlmProvider {
16
+ complete(request: LlmRequest): Promise<LlmResponse>;
17
+ }
18
+
19
+ interface DirectLlmProviderOptions {
20
+ apiKey: string;
21
+ model?: string;
22
+ }
23
+ declare class DirectLlmProvider implements LlmProvider {
24
+ private client;
25
+ private defaultModel;
26
+ constructor(opts: DirectLlmProviderOptions);
27
+ complete(request: LlmRequest): Promise<LlmResponse>;
28
+ }
29
+
30
+ export { DirectLlmProvider, type DirectLlmProviderOptions };
@@ -0,0 +1,30 @@
1
+ interface LlmMessage {
2
+ role: "user" | "assistant";
3
+ content: string;
4
+ }
5
+ interface LlmRequest {
6
+ model?: string;
7
+ messages: LlmMessage[];
8
+ maxTokens?: number;
9
+ systemPrompt?: string;
10
+ }
11
+ interface LlmResponse {
12
+ content: string;
13
+ usedTokens: number;
14
+ }
15
+ interface LlmProvider {
16
+ complete(request: LlmRequest): Promise<LlmResponse>;
17
+ }
18
+
19
+ interface DirectLlmProviderOptions {
20
+ apiKey: string;
21
+ model?: string;
22
+ }
23
+ declare class DirectLlmProvider implements LlmProvider {
24
+ private client;
25
+ private defaultModel;
26
+ constructor(opts: DirectLlmProviderOptions);
27
+ complete(request: LlmRequest): Promise<LlmResponse>;
28
+ }
29
+
30
+ export { DirectLlmProvider, type DirectLlmProviderOptions };
@@ -0,0 +1,30 @@
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ // src/llm/providers/direct.ts
4
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
5
+ var DirectLlmProvider = class {
6
+ client;
7
+ defaultModel;
8
+ constructor(opts) {
9
+ this.client = new Anthropic({ apiKey: opts.apiKey });
10
+ this.defaultModel = opts.model ?? DEFAULT_MODEL;
11
+ }
12
+ async complete(request) {
13
+ const model = request.model ?? this.defaultModel;
14
+ const maxTokens = request.maxTokens ?? 4096;
15
+ const response = await this.client.messages.create({
16
+ model,
17
+ max_tokens: maxTokens,
18
+ system: request.systemPrompt,
19
+ messages: request.messages.map((m) => ({
20
+ role: m.role,
21
+ content: m.content
22
+ }))
23
+ });
24
+ const content = response.content.filter((b) => b.type === "text").map((b) => b.text).join("");
25
+ const usedTokens = (response.usage?.input_tokens ?? 0) + (response.usage?.output_tokens ?? 0);
26
+ return { content, usedTokens };
27
+ }
28
+ };
29
+
30
+ export { DirectLlmProvider };
package/package.json CHANGED
@@ -1,11 +1,61 @@
1
1
  {
2
2
  "name": "forgesmith",
3
- "version": "0.0.1",
4
- "description": "forgesmith — content/asset-generation engine. The lens observes, the smith forges. Sister engine to prismlens. (Reserved package.)",
5
- "main": "index.js",
3
+ "version": "0.2.0",
4
+ "description": "forgesmith — content & asset-generation engine. Forge release notes, blog posts, social copy from prism0x2A data.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.mjs",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.cjs",
14
+ "default": "./dist/index.cjs"
15
+ },
16
+ "./providers": {
17
+ "types": "./dist/providers.d.ts",
18
+ "import": "./dist/providers.mjs",
19
+ "require": "./dist/providers.cjs",
20
+ "default": "./dist/providers.cjs"
21
+ }
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "CHANGELOG.md"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "typecheck": "tsc --noEmit",
33
+ "prepare": "npm run build",
34
+ "prepublishOnly": "npm run build && npm test"
35
+ },
36
+ "dependencies": {
37
+ "@anthropic-ai/sdk": "^0.37.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^22.0.0",
41
+ "tsup": "^8.3.5",
42
+ "typescript": "^5.7.2",
43
+ "vitest": "^2.1.8"
44
+ },
6
45
  "license": "UNLICENSED",
7
46
  "private": false,
8
47
  "homepage": "https://github.com/dadenjo/forgesmith",
9
- "keywords": ["code-intelligence", "content-generation", "release-notes", "marketing", "developer-tools"],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/dadenjo/forgesmith.git"
51
+ },
52
+ "keywords": [
53
+ "code-intelligence",
54
+ "content-generation",
55
+ "release-notes",
56
+ "marketing",
57
+ "developer-tools",
58
+ "prism0x2a"
59
+ ],
10
60
  "author": "dadenjo"
11
61
  }
package/index.js DELETED
@@ -1,5 +0,0 @@
1
- // forgesmith — placeholder reservation. Real release follows prism0x2A v0.3.x.
2
- // The lens observes. The smith forges.
3
- // See: https://github.com/dadenjo/forgesmith
4
- console.log("forgesmith — engine for forge0x2B. Reserved.");
5
- console.log("The lens observes. The smith forges.");