forgesmith 0.6.1 → 0.7.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/dist/server.cjs CHANGED
@@ -1,14 +1,14 @@
1
1
  'use strict';
2
2
 
3
3
  var fs = require('fs/promises');
4
- var path = require('path');
4
+ var path3 = require('path');
5
5
  var fs$1 = require('fs');
6
6
  var crypto = require('crypto');
7
7
 
8
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
10
  var fs__default = /*#__PURE__*/_interopDefault(fs);
11
- var path__default = /*#__PURE__*/_interopDefault(path);
11
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
12
12
 
13
13
  // src/prism/reader.ts
14
14
  async function readJsonFiles(dir) {
@@ -18,7 +18,7 @@ async function readJsonFiles(dir) {
18
18
  for (const entry of entries) {
19
19
  if (!entry.endsWith(".json")) continue;
20
20
  try {
21
- const raw = await fs__default.default.readFile(path__default.default.join(dir, entry), "utf-8");
21
+ const raw = await fs__default.default.readFile(path3__default.default.join(dir, entry), "utf-8");
22
22
  results.push(JSON.parse(raw));
23
23
  } catch {
24
24
  }
@@ -29,9 +29,9 @@ async function readJsonFiles(dir) {
29
29
  }
30
30
  }
31
31
  async function readPrismDirectory(prismPath) {
32
- const sessionsDir = path__default.default.join(prismPath, "sessions");
33
- const recsDir = path__default.default.join(prismPath, "recommendations");
34
- const insightsDir = path__default.default.join(prismPath, "green", "insights", "accepted");
32
+ const sessionsDir = path3__default.default.join(prismPath, "sessions");
33
+ const recsDir = path3__default.default.join(prismPath, "recommendations");
34
+ const insightsDir = path3__default.default.join(prismPath, "green", "insights", "accepted");
35
35
  const [sessions, recommendations, insights] = await Promise.all([
36
36
  readJsonFiles(sessionsDir),
37
37
  readJsonFiles(recsDir),
@@ -40,7 +40,7 @@ async function readPrismDirectory(prismPath) {
40
40
  return { sessions, recommendations, insights };
41
41
  }
42
42
  async function readBlueprintData(targetPath) {
43
- const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
43
+ const snapshotPath = path3__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
44
44
  try {
45
45
  const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
46
46
  return JSON.parse(raw);
@@ -48,6 +48,256 @@ async function readBlueprintData(targetPath) {
48
48
  return null;
49
49
  }
50
50
  }
51
+ function normalise(v, allowed, fallback) {
52
+ if (v && allowed.includes(v)) return v;
53
+ return fallback;
54
+ }
55
+ function parseCapabilitiesMd(content) {
56
+ const match = content.match(/^---\s*\n([\s\S]*?)\n---/m);
57
+ if (!match) return [];
58
+ const yaml = match[1];
59
+ const capsMatch = yaml.match(/^capabilities:\s*\n([\s\S]+)/m);
60
+ if (!capsMatch) return [];
61
+ const body = capsMatch[1];
62
+ const capabilities = [];
63
+ const items = body.split(/\n(?= - )/);
64
+ for (const item of items) {
65
+ if (!item.trim().startsWith("-")) continue;
66
+ const id = extractScalar(item, "id");
67
+ if (!id) continue;
68
+ const name = extractScalar(item, "name") ?? id;
69
+ const criticality = extractScalar(item, "criticality");
70
+ const lifecycle = extractScalar(item, "lifecycle");
71
+ const description = extractMultilineScalar(item, "description");
72
+ capabilities.push({
73
+ id,
74
+ name,
75
+ ...criticality ? { criticality } : {},
76
+ ...lifecycle ? { lifecycle } : {},
77
+ ...description ? { description } : {}
78
+ });
79
+ }
80
+ return capabilities;
81
+ }
82
+ function extractScalar(block, key) {
83
+ const re = new RegExp(`\\n?\\s+${key}:\\s+(.+)`);
84
+ const m = block.match(re);
85
+ if (!m) return void 0;
86
+ return m[1].trim().replace(/^["']|["']$/g, "");
87
+ }
88
+ function extractMultilineScalar(block, key) {
89
+ const re = new RegExp(`\\n?\\s+${key}:\\s+>\\s*\\n((?:[ \\t]+.+\\n?)+)`);
90
+ const m = block.match(re);
91
+ if (!m) return extractScalar(block, key);
92
+ return m[1].split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
93
+ }
94
+ async function readAmberLayer(targetPath) {
95
+ const amberDir = path3__default.default.join(targetPath, ".amber");
96
+ try {
97
+ await fs__default.default.access(amberDir);
98
+ } catch {
99
+ return null;
100
+ }
101
+ let rawCaps = [];
102
+ try {
103
+ const capsMd = await fs__default.default.readFile(
104
+ path3__default.default.join(amberDir, "capabilities.md"),
105
+ "utf-8"
106
+ );
107
+ rawCaps = parseCapabilitiesMd(capsMd);
108
+ } catch {
109
+ return null;
110
+ }
111
+ if (rawCaps.length === 0) return null;
112
+ let state = null;
113
+ try {
114
+ const raw = await fs__default.default.readFile(path3__default.default.join(amberDir, "state.json"), "utf-8");
115
+ state = JSON.parse(raw);
116
+ } catch {
117
+ }
118
+ const stateFiles = state?.files ?? {};
119
+ const allFilePaths = Object.keys(stateFiles);
120
+ const totalFiles = allFilePaths.length;
121
+ const capabilities = rawCaps.map((raw) => {
122
+ const tagged = allFilePaths.filter(
123
+ (fp) => stateFiles[fp]?.capabilities?.includes(raw.id)
124
+ );
125
+ const driftCount = tagged.filter(
126
+ (fp) => !stateFiles[fp]?.doc_hash
127
+ ).length;
128
+ const hasTests = tagged.some((fp) => !!stateFiles[fp]?.doc_hash);
129
+ return {
130
+ id: raw.id,
131
+ name: raw.name,
132
+ criticality: normalise(raw.criticality, ["critical", "high", "medium", "low"], "medium"),
133
+ lifecycle: normalise(raw.lifecycle, ["active", "experimental", "deprecated", "sunset"], "active"),
134
+ ...raw.description ? { description: raw.description } : {},
135
+ files: tagged,
136
+ driftCount,
137
+ hasTests
138
+ };
139
+ });
140
+ const taggedFilesSet = /* @__PURE__ */ new Set();
141
+ for (const fp of allFilePaths) {
142
+ const entry = stateFiles[fp];
143
+ if (entry?.capabilities && entry.capabilities.length > 0) {
144
+ taggedFilesSet.add(fp);
145
+ }
146
+ }
147
+ const taggedFiles = taggedFilesSet.size;
148
+ const taggedPercent = totalFiles > 0 ? Math.round(taggedFiles / totalFiles * 100) : 0;
149
+ const orphanedFiles = allFilePaths.filter((fp) => {
150
+ const entry = stateFiles[fp];
151
+ return !entry?.capabilities || entry.capabilities.length === 0;
152
+ });
153
+ const driftedCapabilities = capabilities.filter((c) => c.driftCount > 0).length;
154
+ const criticalCaps = capabilities.filter((c) => c.criticality === "critical");
155
+ const highCaps = capabilities.filter((c) => c.criticality === "high");
156
+ const summaryParts = [
157
+ `${capabilities.length} business capabilities (${criticalCaps.length} critical, ${highCaps.length} high).`,
158
+ `${taggedFiles}/${totalFiles} files tagged (${taggedPercent}%).`
159
+ ];
160
+ if (driftedCapabilities > 0) {
161
+ summaryParts.push(
162
+ `${driftedCapabilities} capabilit${driftedCapabilities === 1 ? "y has" : "ies have"} documentation drift.`
163
+ );
164
+ }
165
+ if (orphanedFiles.length > 0) {
166
+ summaryParts.push(`${orphanedFiles.length} files are untagged (orphaned).`);
167
+ }
168
+ const topCaps = capabilities.filter((c) => c.criticality === "critical" || c.criticality === "high").slice(0, 3).map((c) => c.name);
169
+ if (topCaps.length > 0) {
170
+ summaryParts.push(`Key capabilities: ${topCaps.join(", ")}.`);
171
+ }
172
+ return {
173
+ capabilities,
174
+ totalFiles,
175
+ taggedFiles,
176
+ taggedPercent,
177
+ driftedCapabilities,
178
+ orphanedFiles,
179
+ summary: summaryParts.join(" ")
180
+ };
181
+ }
182
+ async function readJsonFile(filePath) {
183
+ try {
184
+ const raw = await fs__default.default.readFile(filePath, "utf-8");
185
+ return JSON.parse(raw);
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ async function dirExists(dirPath) {
191
+ try {
192
+ await fs__default.default.access(dirPath);
193
+ return true;
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+ async function readGreenLayer(targetPath) {
199
+ const greenDir = path3__default.default.join(targetPath, ".prism", "green");
200
+ if (!await dirExists(greenDir)) {
201
+ return null;
202
+ }
203
+ const history = await readJsonFile(
204
+ path3__default.default.join(greenDir, "coherence-history.json")
205
+ );
206
+ const latestScore = history?.latest ?? null;
207
+ const coherenceScore = latestScore?.score ?? null;
208
+ const coherenceGrade = latestScore?.grade ?? null;
209
+ const topRisks = (latestScore?.topRisks ?? []).map((r) => ({
210
+ capabilityId: r.capabilityId,
211
+ name: r.name,
212
+ reason: r.reason,
213
+ impact: r.impact
214
+ }));
215
+ const insightStatusMap = await readJsonFile(
216
+ path3__default.default.join(greenDir, "insight-status.json")
217
+ );
218
+ const proposedInsights = await readJsonFile(
219
+ path3__default.default.join(greenDir, "proposed-insights.json")
220
+ );
221
+ const insights = [];
222
+ if (Array.isArray(proposedInsights)) {
223
+ for (const raw of proposedInsights) {
224
+ const id = typeof raw.id === "string" ? raw.id : "";
225
+ if (!id) continue;
226
+ const status = insightStatusMap?.[id];
227
+ if (status === "rejected") continue;
228
+ const title = typeof raw.title === "string" ? raw.title : id;
229
+ const body = typeof raw.body === "string" ? raw.body : typeof raw.description === "string" ? raw.description : "";
230
+ const category = typeof raw.category === "string" ? raw.category : typeof raw.kind === "string" ? raw.kind : "general";
231
+ const severity = typeof raw.severity === "string" ? raw.severity : void 0;
232
+ insights.push({
233
+ id,
234
+ title,
235
+ body,
236
+ category,
237
+ ...severity ? { severity } : {}
238
+ });
239
+ }
240
+ }
241
+ if (insights.length === 0) {
242
+ const acceptedDir = path3__default.default.join(greenDir, "insights", "accepted");
243
+ if (await dirExists(acceptedDir)) {
244
+ try {
245
+ const entries = await fs__default.default.readdir(acceptedDir);
246
+ for (const entry of entries) {
247
+ if (!entry.endsWith(".json")) continue;
248
+ const raw = await readJsonFile(
249
+ path3__default.default.join(acceptedDir, entry)
250
+ );
251
+ if (!raw) continue;
252
+ const id = typeof raw.id === "string" ? raw.id : entry.replace(".json", "");
253
+ const title = typeof raw.title === "string" ? raw.title : id;
254
+ const body = typeof raw.body === "string" ? raw.body : typeof raw.description === "string" ? raw.description : "";
255
+ const category = typeof raw.category === "string" ? raw.category : typeof raw.kind === "string" ? raw.kind : "general";
256
+ const severity = typeof raw.severity === "string" ? raw.severity : void 0;
257
+ insights.push({
258
+ id,
259
+ title,
260
+ body,
261
+ category,
262
+ ...severity ? { severity } : {}
263
+ });
264
+ }
265
+ } catch {
266
+ }
267
+ }
268
+ }
269
+ if (coherenceScore === null && insights.length === 0 && topRisks.length === 0) {
270
+ return null;
271
+ }
272
+ const summaryParts = [];
273
+ if (coherenceScore !== null && coherenceGrade !== null) {
274
+ summaryParts.push(
275
+ `Architecture Coherence Score: ${coherenceScore}/100 (grade ${coherenceGrade}).`
276
+ );
277
+ }
278
+ if (topRisks.length > 0) {
279
+ const topRiskNames = topRisks.slice(0, 3).map((r) => `"${r.name}" (${r.reason})`);
280
+ summaryParts.push(`Top risks: ${topRiskNames.join("; ")}.`);
281
+ }
282
+ if (insights.length > 0) {
283
+ const accepted = insights.filter(
284
+ (i) => insightStatusMap?.[i.id] === "accepted"
285
+ ).length;
286
+ summaryParts.push(
287
+ `${insights.length} cross-layer insight${insights.length === 1 ? "" : "s"}` + (accepted > 0 ? ` (${accepted} accepted)` : "") + "."
288
+ );
289
+ }
290
+ if (summaryParts.length === 0) {
291
+ summaryParts.push("GREEN cross-layer analysis loaded but no actionable data found.");
292
+ }
293
+ return {
294
+ insights,
295
+ coherenceScore,
296
+ coherenceGrade,
297
+ topRisks,
298
+ summary: summaryParts.join(" ")
299
+ };
300
+ }
51
301
 
52
302
  // src/forge/secrets.ts
53
303
  var SECRET_PATTERNS = [
@@ -61,12 +311,86 @@ function scanForSecrets(text) {
61
311
  }
62
312
 
63
313
  // src/forge/blueprintReader.ts
314
+ function isPrismlensShape(raw) {
315
+ if (typeof raw !== "object" || raw === null) return false;
316
+ const r = raw;
317
+ return "generatedAt" in r || "nodes" in r && !("files" in r);
318
+ }
319
+ function normalizePrismlensBlueprint(raw, targetPath = "") {
320
+ if (!isPrismlensShape(raw)) {
321
+ return raw;
322
+ }
323
+ const p = raw;
324
+ const summary = p.summary ?? {};
325
+ const importedByCounts = /* @__PURE__ */ new Map();
326
+ if (Array.isArray(summary.mostImportedFiles)) {
327
+ for (const entry of summary.mostImportedFiles) {
328
+ if (entry && typeof entry.path === "string" && typeof entry.count === "number") {
329
+ importedByCounts.set(entry.path, entry.count);
330
+ }
331
+ }
332
+ }
333
+ const nodePathById = /* @__PURE__ */ new Map();
334
+ const nodes = Array.isArray(p.nodes) ? p.nodes : [];
335
+ for (const node of nodes) {
336
+ if (node.id && node.path) nodePathById.set(node.id, node.path);
337
+ }
338
+ const importCountByPath = /* @__PURE__ */ new Map();
339
+ const prismlensEdges = Array.isArray(p.edges) ? p.edges : [];
340
+ for (const edge of prismlensEdges) {
341
+ const sourcePath = nodePathById.get(edge.source);
342
+ if (sourcePath) {
343
+ importCountByPath.set(sourcePath, (importCountByPath.get(sourcePath) ?? 0) + 1);
344
+ }
345
+ }
346
+ const files = nodes.map((node) => ({
347
+ path: node.path,
348
+ category: node.category ?? "unknown",
349
+ importCount: importCountByPath.get(node.path) ?? 0,
350
+ importedByCount: importedByCounts.get(node.path) ?? 0,
351
+ lineCount: node.loc ?? 0
352
+ }));
353
+ const edges = prismlensEdges.map((e) => ({
354
+ from: nodePathById.get(e.source) ?? e.source,
355
+ to: nodePathById.get(e.target) ?? e.target,
356
+ type: e.type
357
+ }));
358
+ const categories = {
359
+ app: 0,
360
+ component: 0,
361
+ lib: 0,
362
+ hook: 0
363
+ };
364
+ for (const f of files) {
365
+ const cat = f.category ?? "unknown";
366
+ categories[cat] = (categories[cat] ?? 0) + 1;
367
+ }
368
+ if (typeof summary.componentCount === "number") categories.component = summary.componentCount;
369
+ if (typeof summary.libCount === "number") categories.lib = summary.libCount;
370
+ if (typeof summary.hookCount === "number") categories.hook = summary.hookCount;
371
+ if (typeof summary.pageCount === "number") categories.app = summary.pageCount;
372
+ const totalFiles = summary.fileCount ?? files.length;
373
+ const runtimeEdges = summary.runtimeInternalDependencyCount ?? summary.edgeCount ?? edges.length;
374
+ let scanTimestamp = Math.floor(Date.now() / 1e3);
375
+ if (typeof p.generatedAt === "string") {
376
+ const parsed = Date.parse(p.generatedAt);
377
+ if (!Number.isNaN(parsed)) scanTimestamp = Math.floor(parsed / 1e3);
378
+ }
379
+ return {
380
+ targetPath,
381
+ scanTimestamp,
382
+ stats: { totalFiles, runtimeEdges },
383
+ files,
384
+ edges,
385
+ categories
386
+ };
387
+ }
64
388
  var ALLOWED_BLUEPRINT_FILENAMES = /* @__PURE__ */ new Set(["blueprint.json", "snapshot.json"]);
65
389
  function isAllowedBlueprintPath(targetPath, filePath) {
66
- const resolvedTarget = path.resolve(targetPath);
67
- const resolvedFile = path.resolve(filePath);
68
- const allowedPrefix = path.join(resolvedTarget, ".prism", "blueprint");
69
- if (!resolvedFile.startsWith(allowedPrefix + "/") && resolvedFile !== allowedPrefix) {
390
+ const resolvedTarget = path3.resolve(targetPath);
391
+ const resolvedFile = path3.resolve(filePath);
392
+ const prismRoot = path3.join(resolvedTarget, ".prism");
393
+ if (!resolvedFile.startsWith(prismRoot + "/") && resolvedFile !== prismRoot) {
70
394
  return false;
71
395
  }
72
396
  const basename = resolvedFile.slice(resolvedFile.lastIndexOf("/") + 1);
@@ -74,16 +398,22 @@ function isAllowedBlueprintPath(targetPath, filePath) {
74
398
  }
75
399
  function readBlueprintFromTarget(targetPath) {
76
400
  const candidates = [
77
- path.join(targetPath, ".prism", "blueprint", "snapshot.json"),
78
- path.join(targetPath, ".prism", "blueprint.json")
401
+ { path: path3.join(targetPath, ".prism", "blueprint", "snapshot.json"), isPrismlens: false },
402
+ { path: path3.join(targetPath, ".prism", "blueprint.json"), isPrismlens: true }
79
403
  ];
80
- for (const candidate of candidates) {
81
- const normalized = path.normalize(candidate);
404
+ for (const { path: candidate, isPrismlens } of candidates) {
405
+ const normalized = path3.normalize(candidate);
82
406
  if (!isAllowedBlueprintPath(targetPath, normalized)) continue;
83
407
  if (!fs$1.existsSync(normalized)) continue;
84
408
  try {
85
409
  const raw = fs$1.readFileSync(normalized, "utf-8");
86
- return JSON.parse(raw);
410
+ const parsed = JSON.parse(raw);
411
+ if (isPrismlens || isPrismlensShape(parsed)) {
412
+ console.log(`[forgesmith] blueprint: reading prismlens shape from ${normalized}`);
413
+ return normalizePrismlensBlueprint(parsed, targetPath);
414
+ }
415
+ console.log(`[forgesmith] blueprint: reading native snapshot from ${normalized}`);
416
+ return parsed;
87
417
  } catch {
88
418
  return null;
89
419
  }
@@ -178,7 +508,7 @@ function deriveAudienceMix(categories, paths) {
178
508
  return "developer+business";
179
509
  }
180
510
  async function extractBrandFromPrismBlueprint(targetPath) {
181
- const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
511
+ const snapshotPath = path3__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
182
512
  let blueprint = {};
183
513
  try {
184
514
  const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
@@ -191,7 +521,7 @@ async function extractBrandFromPrismBlueprint(targetPath) {
191
521
  const total = blueprint.stats?.totalFiles ?? files.length;
192
522
  const technicalScore = deriveTechnicalScore(categories, total);
193
523
  const audienceMix = deriveAudienceMix(categories, paths);
194
- const projectName = blueprint.targetPath ? path__default.default.basename(blueprint.targetPath) : "Project";
524
+ const projectName = blueprint.targetPath ? path3__default.default.basename(blueprint.targetPath) : "Project";
195
525
  const formality = technicalScore > 60 ? 65 : 50;
196
526
  const technicality = Math.min(95, technicalScore + 10);
197
527
  return {
@@ -372,12 +702,12 @@ User wants to highlight: ${highlight}`;
372
702
  function buildSystemPrompt() {
373
703
  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.`;
374
704
  }
375
- function buildUserPrompt(data, opts) {
705
+ function buildUserPrompt(data, opts, amberContext, greenContext) {
376
706
  const tone = opts.tone ?? "professional";
377
707
  const length = opts.length ?? "medium";
378
708
  const format = opts.format ?? "markdown";
379
709
  const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
380
- const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
710
+ const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric", executive: "high-level, strategic, board-ready summary" }[tone] ?? "formal, clear, business-appropriate";
381
711
  const hasSessions = (data.sessions?.length ?? 0) > 0;
382
712
  const hasRecs = (data.recommendations?.length ?? 0) > 0;
383
713
  const hasInsights = (data.insights?.length ?? 0) > 0;
@@ -413,11 +743,38 @@ function buildUserPrompt(data, opts) {
413
743
  }
414
744
  lines.push(``);
415
745
  }
746
+ if (amberContext && amberContext.capabilities.length > 0) {
747
+ lines.push(`## Business Capabilities (AMBER layer)`);
748
+ lines.push(amberContext.summary);
749
+ const criticalCaps = amberContext.capabilities.filter(
750
+ (c) => c.criticality === "critical" || c.criticality === "high"
751
+ );
752
+ if (criticalCaps.length > 0) {
753
+ lines.push(
754
+ `This release may touch these capabilities: ${criticalCaps.map((c) => c.name).join(", ")}.`
755
+ );
756
+ }
757
+ lines.push(``);
758
+ }
759
+ if (greenContext) {
760
+ if (greenContext.coherenceScore !== null) {
761
+ lines.push(`## Architecture Health (GREEN layer)`);
762
+ lines.push(
763
+ `Coherence score: ${greenContext.coherenceScore}/100 (grade ${greenContext.coherenceGrade ?? "?"}).`
764
+ );
765
+ if (greenContext.topRisks.length > 0) {
766
+ lines.push(
767
+ `Top risks: ${greenContext.topRisks.slice(0, 3).map((r) => `${r.name} \u2014 ${r.reason}`).join("; ")}.`
768
+ );
769
+ }
770
+ lines.push(``);
771
+ }
772
+ }
416
773
  return lines.join("\n");
417
774
  }
418
- async function generateReleaseNotes(data, opts, provider) {
775
+ async function generateReleaseNotes(data, opts, provider, amberContext, greenContext) {
419
776
  const systemPrompt = buildSystemPrompt();
420
- const userPrompt = buildUserPrompt(data, opts);
777
+ const userPrompt = buildUserPrompt(data, opts, amberContext, greenContext);
421
778
  const response = await provider.complete({
422
779
  systemPrompt,
423
780
  messages: [{ role: "user", content: userPrompt }],
@@ -437,13 +794,16 @@ async function generateReleaseNotes(data, opts, provider) {
437
794
  var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
438
795
  function buildSystemPrompt2(tone) {
439
796
  const toneMap = {
440
- technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
797
+ professional: "You are a senior software architect who writes clear, professional documentation suitable for all engineering audiences.",
441
798
  casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
799
+ technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
800
+ executive: "You are a CTO-level advisor who explains codebase architecture at a high strategic level for leadership audiences.",
801
+ // Legacy aliases kept for backwards compatibility
442
802
  onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
443
803
  };
444
- return `${toneMap[tone] ?? toneMap.technical} Write only the requested document \u2014 no preamble, no meta-commentary.`;
804
+ return `${toneMap[tone] ?? toneMap.professional} Write only the requested document \u2014 no preamble, no meta-commentary.`;
445
805
  }
446
- function buildUserPrompt2(blueprint, opts) {
806
+ function buildUserPrompt2(blueprint, opts, amberContext) {
447
807
  const tone = opts.tone ?? "technical";
448
808
  const length = opts.length ?? "long";
449
809
  const format = opts.format ?? "markdown";
@@ -471,9 +831,28 @@ function buildUserPrompt2(blueprint, opts) {
471
831
  ``,
472
832
  `Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
473
833
  ];
834
+ if (amberContext && amberContext.capabilities.length > 0) {
835
+ lines.push(``);
836
+ lines.push(`## Business Capabilities (AMBER layer)`);
837
+ lines.push(amberContext.summary);
838
+ lines.push(``);
839
+ const sorted = amberContext.capabilities.slice().sort((a, b) => {
840
+ const rank = { critical: 0, high: 1, medium: 2, low: 3 };
841
+ return (rank[a.criticality] ?? 4) - (rank[b.criticality] ?? 4);
842
+ });
843
+ for (const cap of sorted) {
844
+ const tags = [cap.criticality, cap.lifecycle].filter(Boolean).join(", ");
845
+ const fileCount = cap.files.length;
846
+ lines.push(
847
+ `- **${cap.name}** (${cap.id}) [${tags}]${fileCount > 0 ? ` \u2014 ${fileCount} file${fileCount === 1 ? "" : "s"}` : ""}`
848
+ );
849
+ if (cap.description) lines.push(` ${cap.description}`);
850
+ }
851
+ lines.push(`Include a "Business Capabilities" section in the walkthrough mapping these capabilities to the codebase.`);
852
+ }
474
853
  return lines.join("\n");
475
854
  }
476
- async function generateArchitectureWalkthrough(blueprint, opts, provider) {
855
+ async function generateArchitectureWalkthrough(blueprint, opts, provider, amberContext) {
477
856
  if (!blueprint) {
478
857
  return {
479
858
  text: NO_DATA_MSG,
@@ -482,7 +861,7 @@ async function generateArchitectureWalkthrough(blueprint, opts, provider) {
482
861
  }
483
862
  const response = await provider.complete({
484
863
  systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
485
- messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
864
+ messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts, amberContext) }],
486
865
  maxTokens: 4096
487
866
  });
488
867
  return {
@@ -557,11 +936,15 @@ async function generateChangesSince(current, previous, opts, provider) {
557
936
  var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
558
937
  function buildSystemPrompt4(tone) {
559
938
  const toneMap = {
939
+ professional: "You are a documentation engineer writing a structured, professional onboarding reference for engineering teams.",
940
+ casual: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
941
+ technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals.",
942
+ executive: "You are a technical lead writing a concise onboarding overview suitable for both technical and leadership audiences.",
943
+ // Legacy aliases kept for backwards compatibility
560
944
  friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
561
- formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams.",
562
- technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
945
+ formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams."
563
946
  };
564
- return `${toneMap[tone] ?? toneMap.friendly} Write only the document \u2014 no preamble, no meta-commentary.`;
947
+ return `${toneMap[tone] ?? toneMap.professional} Write only the document \u2014 no preamble, no meta-commentary.`;
565
948
  }
566
949
  function buildUserPrompt4(blueprint, opts) {
567
950
  const tone = opts.tone ?? "friendly";
@@ -622,11 +1005,14 @@ async function generateOnboardingDoc(blueprint, opts, provider) {
622
1005
  var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
623
1006
  function buildSystemPrompt5(tone) {
624
1007
  const toneMap = {
625
- analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
1008
+ professional: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
626
1009
  casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
627
- executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership."
1010
+ technical: "You are a senior engineer who produces detailed, implementation-focused refactoring analysis with concrete code-level recommendations.",
1011
+ executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership.",
1012
+ // Legacy aliases kept for backwards compatibility
1013
+ analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization."
628
1014
  };
629
- return `${toneMap[tone] ?? toneMap.analytical} Write only the report \u2014 no preamble, no meta-commentary.`;
1015
+ return `${toneMap[tone] ?? toneMap.professional} Write only the report \u2014 no preamble, no meta-commentary.`;
630
1016
  }
631
1017
  function detectImportCycles(edges) {
632
1018
  const graph = /* @__PURE__ */ new Map();
@@ -634,158 +1020,2524 @@ function detectImportCycles(edges) {
634
1020
  if (!graph.has(e.from)) graph.set(e.from, /* @__PURE__ */ new Set());
635
1021
  graph.get(e.from).add(e.to);
636
1022
  }
637
- const cycles = [];
638
- const visited = /* @__PURE__ */ new Set();
639
- const stack = /* @__PURE__ */ new Set();
640
- function dfs(node, path3) {
641
- if (cycles.length >= 5) return;
642
- if (stack.has(node)) {
643
- const cycleStart = path3.indexOf(node);
644
- if (cycleStart !== -1) {
645
- cycles.push(path3.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
1023
+ const cycles = [];
1024
+ const visited = /* @__PURE__ */ new Set();
1025
+ const stack = /* @__PURE__ */ new Set();
1026
+ function dfs(node, path5) {
1027
+ if (cycles.length >= 5) return;
1028
+ if (stack.has(node)) {
1029
+ const cycleStart = path5.indexOf(node);
1030
+ if (cycleStart !== -1) {
1031
+ cycles.push(path5.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
1032
+ }
1033
+ return;
1034
+ }
1035
+ if (visited.has(node)) return;
1036
+ visited.add(node);
1037
+ stack.add(node);
1038
+ for (const neighbor of graph.get(node) ?? []) {
1039
+ dfs(neighbor, [...path5, node]);
1040
+ }
1041
+ stack.delete(node);
1042
+ }
1043
+ for (const node of graph.keys()) {
1044
+ if (!visited.has(node)) dfs(node, []);
1045
+ }
1046
+ return cycles;
1047
+ }
1048
+ function buildUserPrompt5(blueprint, opts, amberContext, greenContext) {
1049
+ const tone = opts.tone ?? "analytical";
1050
+ const length = opts.length ?? "medium";
1051
+ const format = opts.format ?? "markdown";
1052
+ const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
1053
+ const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
1054
+ const importCycles = detectImportCycles(blueprint.edges);
1055
+ const lines = [
1056
+ `Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
1057
+ `Tone: ${tone}. Length: ${length}.`,
1058
+ ``,
1059
+ `## Architecture Metrics`,
1060
+ `- Total files: ${blueprint.stats.totalFiles}`,
1061
+ `- Dependency edges: ${blueprint.stats.runtimeEdges}`,
1062
+ `- Categories: ${JSON.stringify(blueprint.categories)}`,
1063
+ ``,
1064
+ `## High-Coupling Hot Spots (high total connections)`,
1065
+ ...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
1066
+ ``,
1067
+ `## High Fan-Out Files (many outgoing dependencies)`,
1068
+ ...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
1069
+ ``,
1070
+ `## Import Cycles Detected (${importCycles.length})`,
1071
+ ...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
1072
+ ``,
1073
+ `Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
1074
+ ];
1075
+ if (amberContext && amberContext.capabilities.length > 0) {
1076
+ lines.push(``);
1077
+ lines.push(`## Business Capabilities with Documentation Drift (AMBER layer)`);
1078
+ lines.push(amberContext.summary);
1079
+ const drifted = amberContext.capabilities.filter((c) => c.driftCount > 0);
1080
+ if (drifted.length > 0) {
1081
+ lines.push(`Capabilities with drift (files changed without updating @amber-doc):`);
1082
+ for (const cap of drifted) {
1083
+ lines.push(
1084
+ `- **${cap.name}** (${cap.id}, ${cap.criticality}): ${cap.driftCount} file${cap.driftCount === 1 ? "" : "s"} drifted`
1085
+ );
1086
+ }
1087
+ }
1088
+ }
1089
+ if (greenContext) {
1090
+ lines.push(``);
1091
+ lines.push(`## Cross-Layer Risks (GREEN layer)`);
1092
+ lines.push(greenContext.summary);
1093
+ if (greenContext.topRisks.length > 0) {
1094
+ lines.push(`Top architectural risks:`);
1095
+ for (const risk of greenContext.topRisks) {
1096
+ lines.push(
1097
+ `- **${risk.name}** (impact: ${risk.impact}): ${risk.reason}`
1098
+ );
1099
+ }
1100
+ }
1101
+ if (greenContext.insights.length > 0) {
1102
+ lines.push(`Cross-layer insights:`);
1103
+ for (const insight of greenContext.insights.slice(0, 5)) {
1104
+ lines.push(`- **${insight.title}**: ${insight.body}`);
1105
+ }
1106
+ }
1107
+ }
1108
+ return lines.join("\n");
1109
+ }
1110
+ async function generateRefactoringReport(blueprint, opts, provider, amberContext, greenContext) {
1111
+ if (!blueprint) {
1112
+ return {
1113
+ text: NO_DATA_MSG4,
1114
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
1115
+ };
1116
+ }
1117
+ const response = await provider.complete({
1118
+ systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
1119
+ messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts, amberContext, greenContext) }],
1120
+ maxTokens: 3072
1121
+ });
1122
+ return {
1123
+ text: response.content,
1124
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
1125
+ };
1126
+ }
1127
+
1128
+ // src/generators/askDrivenAsset.ts
1129
+ var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
1130
+ var FORMAT_GUIDES = {
1131
+ markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
1132
+ blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
1133
+ social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
1134
+ email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
1135
+ slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
1136
+ slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
1137
+ };
1138
+ function buildSystemPrompt6(format, tone) {
1139
+ const toneMap = {
1140
+ professional: "You are a professional technical writer and developer advocate.",
1141
+ casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
1142
+ technical: "You are a senior software engineer writing precise, implementation-focused content.",
1143
+ executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
1144
+ };
1145
+ const guide = FORMAT_GUIDES[format];
1146
+ return `${toneMap[tone] ?? toneMap.professional} Generate a ${guide.name} based on the user's question and the provided codebase architecture context. ${guide.structure} Write only the requested content \u2014 no preamble, no meta-commentary.`;
1147
+ }
1148
+ function buildUserPrompt6(blueprint, question, opts) {
1149
+ const format = opts.format ?? "markdown";
1150
+ const tone = opts.tone ?? "professional";
1151
+ const length = opts.length ?? "medium";
1152
+ const guide = FORMAT_GUIDES[format];
1153
+ const lengthGuide = {
1154
+ short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
1155
+ medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
1156
+ long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
1157
+ }[length];
1158
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
1159
+ const lines = [
1160
+ `## User's Question`,
1161
+ question,
1162
+ ``,
1163
+ `## Output Requirements`,
1164
+ `- Format: ${guide.name}`,
1165
+ `- Tone: ${tone}`,
1166
+ `- Length: ${length} (${lengthGuide})`,
1167
+ ``,
1168
+ `## Codebase Architecture Context`,
1169
+ `Target: ${blueprint.targetPath}`,
1170
+ `Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
1171
+ `Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
1172
+ ``,
1173
+ `Key files (by usage):`,
1174
+ ...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
1175
+ ];
1176
+ if (blueprint.edges.length > 0) {
1177
+ const edgeSample = blueprint.edges.slice(0, 15);
1178
+ lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
1179
+ }
1180
+ lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
1181
+ return lines.join("\n");
1182
+ }
1183
+ async function generateAskDrivenAsset(blueprint, question, opts, provider) {
1184
+ if (!blueprint) {
1185
+ return {
1186
+ text: NO_DATA_MSG5,
1187
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
1188
+ };
1189
+ }
1190
+ if (!question || !question.trim()) {
1191
+ return {
1192
+ text: "No question provided. Please ask something about your codebase.",
1193
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
1194
+ };
1195
+ }
1196
+ const format = opts.format ?? "markdown";
1197
+ const tone = opts.tone ?? "professional";
1198
+ const maxTokens = FORMAT_GUIDES[format].maxTokens;
1199
+ const response = await provider.complete({
1200
+ systemPrompt: buildSystemPrompt6(format, tone),
1201
+ messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
1202
+ maxTokens
1203
+ });
1204
+ return {
1205
+ text: response.content,
1206
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
1207
+ };
1208
+ }
1209
+
1210
+ // src/generators/generatePresentation.ts
1211
+ function buildContext(blueprint, opts) {
1212
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
1213
+ const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
1214
+ const totalLoc = blueprint.files.reduce(
1215
+ (sum, f) => sum + (f.lineCount ?? 0),
1216
+ 0
1217
+ );
1218
+ const lines = [
1219
+ `## Codebase Context`,
1220
+ `Project: ${projectName}`,
1221
+ `Path: ${blueprint.targetPath}`,
1222
+ `Total files: ${blueprint.stats.totalFiles}`,
1223
+ `Total lines of code: ${totalLoc}`,
1224
+ `Dependency edges: ${blueprint.stats.runtimeEdges}`,
1225
+ `Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
1226
+ ``,
1227
+ `## Most-imported files`,
1228
+ ...topFiles.map(
1229
+ (f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, lines: ${f.lineCount ?? "?"}`
1230
+ )
1231
+ ];
1232
+ if (blueprint.edges.length > 0) {
1233
+ const edgeSample = blueprint.edges.slice(0, 10);
1234
+ lines.push(
1235
+ ``,
1236
+ `## Dependency sample`,
1237
+ ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`)
1238
+ );
1239
+ }
1240
+ return lines.join("\n");
1241
+ }
1242
+ function planSlideTypes(opts) {
1243
+ const { slideCount, focusArea, audience } = opts;
1244
+ const focus = focusArea ?? "all";
1245
+ const always = ["title", "closing"];
1246
+ let middle = [];
1247
+ if (focus === "architecture") {
1248
+ middle = ["executive", "architecture", "metrics", "risks", "recommendations"];
1249
+ } else if (focus === "health") {
1250
+ middle = ["executive", "health", "risks", "recommendations", "metrics"];
1251
+ } else if (focus === "risks") {
1252
+ middle = ["executive", "risks", "recommendations", "architecture", "health"];
1253
+ } else {
1254
+ if (audience === "executive") {
1255
+ middle = ["executive", "health", "risks", "recommendations", "architecture", "metrics", "capabilities", "recommendations"];
1256
+ } else if (audience === "technical") {
1257
+ middle = ["architecture", "metrics", "risks", "health", "recommendations", "capabilities", "executive", "recommendations"];
1258
+ } else {
1259
+ middle = ["executive", "architecture", "health", "risks", "recommendations", "metrics", "capabilities", "executive"];
1260
+ }
1261
+ }
1262
+ const targetMiddle = slideCount - 2;
1263
+ const trimmed = middle.slice(0, targetMiddle);
1264
+ const seen = /* @__PURE__ */ new Set();
1265
+ const deduped = [];
1266
+ for (const t of trimmed) {
1267
+ if (!seen.has(t)) {
1268
+ seen.add(t);
1269
+ deduped.push(t);
1270
+ }
1271
+ }
1272
+ return [always[0], ...deduped, always[1]];
1273
+ }
1274
+ function buildSystemPrompt7(opts) {
1275
+ const toneMap = {
1276
+ professional: "You are a senior engineering consultant who creates compelling, data-driven presentations for technical and business stakeholders.",
1277
+ casual: "You are a developer advocate who creates friendly, approachable presentations that make architecture exciting.",
1278
+ executive: "You are a VP of Engineering who creates concise, business-value-focused presentations for C-suite audiences."
1279
+ };
1280
+ return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
1281
+ }
1282
+ function buildUserPrompt7(blueprint, opts, slideTypes) {
1283
+ const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
1284
+ const context = buildContext(blueprint, opts);
1285
+ const slideTypeDescriptions = {
1286
+ title: "Opening slide with project name, tagline, and date. Fields: title (project name), subtitle (tagline), speakerNotes",
1287
+ executive: "Executive summary with 3 key findings. Fields: title, bullets (exactly 3 bullet strings), speakerNotes",
1288
+ architecture: "Architecture overview. Fields: title, subtitle (layer summary), bullets (up to 5 key architecture facts), visualHint (describe a diagram), speakerNotes",
1289
+ capabilities: "Capability map overview. Fields: title, bullets (up to 5 capability areas found), speakerNotes",
1290
+ health: "Codebase health score. Fields: title, highlight (a letter grade A-F or numeric score), highlightLabel, bullets (up to 3 health indicators), speakerNotes",
1291
+ risks: "Top risks. Fields: title, bullets (exactly 3 risks, each prefixed with High/Medium/Low impact), speakerNotes",
1292
+ recommendations: "Action items. Fields: title, bullets (up to 5 concrete action items), speakerNotes",
1293
+ metrics: "Key metrics. Fields: title, highlight (most important metric), highlightLabel, bullets (up to 5 metrics), speakerNotes",
1294
+ closing: "Next steps and call to action. Fields: title, subtitle, bullets (up to 3 next steps), speakerNotes"
1295
+ };
1296
+ const slidePlan = slideTypes.map((t, i) => ` ${i + 1}. type="${t}" \u2014 ${slideTypeDescriptions[t]}`).join("\n");
1297
+ return [
1298
+ `Generate a ${opts.slideCount}-slide presentation deck for: ${projectName}`,
1299
+ `Audience: ${opts.audience}. Tone: ${opts.tone}. Theme: ${opts.theme}.`,
1300
+ ``,
1301
+ context,
1302
+ ``,
1303
+ `## Slide Plan (${slideTypes.length} slides)`,
1304
+ slidePlan,
1305
+ ``,
1306
+ `## Output Format`,
1307
+ `Return a single JSON object with this exact shape:`,
1308
+ `{`,
1309
+ ` "title": "<deck title>",`,
1310
+ ` "subtitle": "<deck subtitle>",`,
1311
+ ` "slides": [`,
1312
+ ` {`,
1313
+ ` "type": "<one of the PresentationSlideType values>",`,
1314
+ ` "title": "<slide title>",`,
1315
+ ` "subtitle": "<optional subtitle>",`,
1316
+ ` "bullets": ["<bullet 1>", "<bullet 2>"],`,
1317
+ ` "highlight": "<optional large callout>",`,
1318
+ ` "highlightLabel": "<optional label>",`,
1319
+ ` "speakerNotes": "<presenter notes>",`,
1320
+ ` "visualHint": "<optional visual description>"`,
1321
+ ` }`,
1322
+ ` ]`,
1323
+ `}`,
1324
+ ``,
1325
+ `Rules:`,
1326
+ `- Each slide in the output MUST match the type listed in the slide plan above.`,
1327
+ `- bullets array: max 5 items, each \u2264 80 characters.`,
1328
+ `- speakerNotes: 1-3 sentences a presenter would say.`,
1329
+ `- Derive all content from the codebase context above \u2014 no invented facts.`,
1330
+ `- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
1331
+ ].join("\n");
1332
+ }
1333
+ function buildFallback(blueprint, opts, slideTypes) {
1334
+ const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
1335
+ const totalLoc = blueprint.files.reduce(
1336
+ (sum, f) => sum + (f.lineCount ?? 0),
1337
+ 0
1338
+ );
1339
+ const slideBuilders = {
1340
+ title: () => ({
1341
+ type: "title",
1342
+ title: projectName,
1343
+ subtitle: "Architecture & Code Intelligence Report",
1344
+ speakerNotes: `Welcome. Today we cover the architecture of ${projectName}.`
1345
+ }),
1346
+ executive: () => ({
1347
+ type: "executive",
1348
+ title: "Executive Summary",
1349
+ bullets: [
1350
+ `${blueprint.stats.totalFiles} files analyzed across all layers`,
1351
+ `${blueprint.stats.runtimeEdges} dependency edges mapped`,
1352
+ `Architecture categories: app, components, lib, hooks`
1353
+ ],
1354
+ speakerNotes: "Three key findings from the codebase scan."
1355
+ }),
1356
+ architecture: () => ({
1357
+ type: "architecture",
1358
+ title: "Architecture Overview",
1359
+ subtitle: `${blueprint.stats.totalFiles} files, ${blueprint.stats.runtimeEdges} edges`,
1360
+ bullets: [
1361
+ `App layer: ${blueprint.categories.app ?? 0} files`,
1362
+ `Components: ${blueprint.categories.component ?? 0} files`,
1363
+ `Libraries: ${blueprint.categories.lib ?? 0} files`,
1364
+ `Hooks: ${blueprint.categories.hook ?? 0} files`
1365
+ ],
1366
+ visualHint: "Layered architecture diagram with dependency arrows",
1367
+ speakerNotes: "The codebase is organized into four primary layers."
1368
+ }),
1369
+ capabilities: () => ({
1370
+ type: "capabilities",
1371
+ title: "Capability Map",
1372
+ bullets: ["Core application logic", "Component layer", "Shared utilities", "Custom hooks"],
1373
+ speakerNotes: "These are the key capability areas identified in the scan."
1374
+ }),
1375
+ health: () => ({
1376
+ type: "health",
1377
+ title: "Codebase Health",
1378
+ highlight: "B",
1379
+ highlightLabel: "Overall Grade",
1380
+ bullets: [
1381
+ `${blueprint.stats.totalFiles} total files`,
1382
+ `${blueprint.stats.runtimeEdges} dependency edges`,
1383
+ `Avg LOC per file: ${blueprint.stats.totalFiles > 0 ? Math.round(totalLoc / blueprint.stats.totalFiles) : 0}`
1384
+ ],
1385
+ speakerNotes: "The codebase shows good structural health with opportunities for improvement."
1386
+ }),
1387
+ risks: () => ({
1388
+ type: "risks",
1389
+ title: "Top Risks",
1390
+ bullets: [
1391
+ "High impact: Large files with many incoming dependencies",
1392
+ "Medium impact: Missing test coverage in critical paths",
1393
+ "Low impact: Inconsistent category classification"
1394
+ ],
1395
+ speakerNotes: "Three risks identified, prioritized by impact."
1396
+ }),
1397
+ recommendations: () => ({
1398
+ type: "recommendations",
1399
+ title: "Recommendations",
1400
+ bullets: [
1401
+ "Break down files with high importedByCount into smaller modules",
1402
+ "Establish clear boundaries between app, lib, and component layers",
1403
+ "Add test coverage to files with the most incoming dependencies",
1404
+ "Review and resolve circular dependency patterns",
1405
+ "Document architecture decisions for new team members"
1406
+ ],
1407
+ speakerNotes: "Five concrete action items, ordered by priority."
1408
+ }),
1409
+ metrics: () => ({
1410
+ type: "metrics",
1411
+ title: "Key Metrics",
1412
+ highlight: String(blueprint.stats.totalFiles),
1413
+ highlightLabel: "Total Files",
1414
+ bullets: [
1415
+ `Lines of code: ${totalLoc.toLocaleString()}`,
1416
+ `Dependency edges: ${blueprint.stats.runtimeEdges}`,
1417
+ `App files: ${blueprint.categories.app ?? 0}`,
1418
+ `Components: ${blueprint.categories.component ?? 0}`,
1419
+ `Libraries: ${blueprint.categories.lib ?? 0}`
1420
+ ],
1421
+ speakerNotes: "Key quantitative metrics from the Blueprint scan."
1422
+ }),
1423
+ closing: () => ({
1424
+ type: "closing",
1425
+ title: "Next Steps",
1426
+ subtitle: "Start with the highest-impact recommendations",
1427
+ bullets: [
1428
+ "Schedule architecture review with the team",
1429
+ "Prioritize the top 3 recommendations",
1430
+ "Set up regular prism0x2A scans"
1431
+ ],
1432
+ speakerNotes: "Thank you. Questions?"
1433
+ })
1434
+ };
1435
+ const slides = slideTypes.map((t) => slideBuilders[t]());
1436
+ return {
1437
+ title: `${projectName} \u2014 Architecture Deck`,
1438
+ subtitle: `${opts.audience.charAt(0).toUpperCase() + opts.audience.slice(1)} presentation \xB7 ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`,
1439
+ slideCount: slides.length,
1440
+ slides,
1441
+ theme: opts.theme,
1442
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1443
+ };
1444
+ }
1445
+ function parseLlmResponse(raw, slideTypes, opts, blueprint) {
1446
+ let text = raw.trim();
1447
+ if (text.startsWith("```")) {
1448
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
1449
+ }
1450
+ let parsed;
1451
+ try {
1452
+ parsed = JSON.parse(text);
1453
+ } catch {
1454
+ return null;
1455
+ }
1456
+ const rawSlides = Array.isArray(parsed.slides) ? parsed.slides : [];
1457
+ if (rawSlides.length === 0) return null;
1458
+ const slides = rawSlides.map((s, i) => {
1459
+ const slide = s ?? {};
1460
+ const inferredType = typeof slide.type === "string" && [
1461
+ "title",
1462
+ "executive",
1463
+ "architecture",
1464
+ "capabilities",
1465
+ "health",
1466
+ "risks",
1467
+ "recommendations",
1468
+ "metrics",
1469
+ "closing"
1470
+ ].includes(slide.type) ? slide.type : slideTypes[i] ?? "executive";
1471
+ const bullets = Array.isArray(slide.bullets) ? slide.bullets.filter((b) => typeof b === "string").slice(0, 5) : void 0;
1472
+ return {
1473
+ type: inferredType,
1474
+ title: typeof slide.title === "string" ? slide.title : `Slide ${i + 1}`,
1475
+ ...typeof slide.subtitle === "string" && slide.subtitle ? { subtitle: slide.subtitle } : {},
1476
+ ...bullets && bullets.length > 0 ? { bullets } : {},
1477
+ ...typeof slide.highlight === "string" && slide.highlight ? { highlight: slide.highlight } : {},
1478
+ ...typeof slide.highlightLabel === "string" && slide.highlightLabel ? { highlightLabel: slide.highlightLabel } : {},
1479
+ ...typeof slide.speakerNotes === "string" && slide.speakerNotes ? { speakerNotes: slide.speakerNotes } : {},
1480
+ ...typeof slide.visualHint === "string" && slide.visualHint ? { visualHint: slide.visualHint } : {}
1481
+ };
1482
+ });
1483
+ const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
1484
+ return {
1485
+ title: typeof parsed.title === "string" && parsed.title ? parsed.title : `${projectName} \u2014 Architecture Deck`,
1486
+ subtitle: typeof parsed.subtitle === "string" && parsed.subtitle ? parsed.subtitle : `${opts.audience} presentation`,
1487
+ slideCount: slides.length,
1488
+ slides,
1489
+ theme: opts.theme,
1490
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
1491
+ };
1492
+ }
1493
+ async function generatePresentation(opts) {
1494
+ const { blueprint, llm } = opts;
1495
+ const slideTypes = planSlideTypes(opts);
1496
+ try {
1497
+ const response = await llm.complete({
1498
+ systemPrompt: buildSystemPrompt7(opts),
1499
+ messages: [
1500
+ {
1501
+ role: "user",
1502
+ content: buildUserPrompt7(blueprint, opts, slideTypes)
1503
+ }
1504
+ ],
1505
+ maxTokens: 4096
1506
+ });
1507
+ const parsed = parseLlmResponse(response.content, slideTypes, opts, blueprint);
1508
+ if (parsed) return parsed;
1509
+ } catch {
1510
+ }
1511
+ return buildFallback(blueprint, opts, slideTypes);
1512
+ }
1513
+
1514
+ // src/generators/generateComplianceDoc.ts
1515
+ var FRAMEWORK_SPECS = {
1516
+ SOX: {
1517
+ fullName: "Sarbanes-Oxley Act (SOX)",
1518
+ focusAreas: [
1519
+ "Internal controls over financial reporting (ICFR)",
1520
+ "Access control and segregation of duties",
1521
+ "Change management procedures",
1522
+ "Audit trail completeness",
1523
+ "IT general controls (ITGCs)"
1524
+ ],
1525
+ requiredSections: [
1526
+ "Scope and System Boundaries",
1527
+ "IT General Controls",
1528
+ "Access Control and Segregation of Duties",
1529
+ "Change Management",
1530
+ "Audit Trail and Logging",
1531
+ "Financial Reporting Controls"
1532
+ ],
1533
+ sectionGuidance: "Focus on Section 302 (CEO/CFO certification) and Section 404 (management assessment of internal controls). Identify which capabilities handle financial data, who can access them, and whether audit trails are complete."
1534
+ },
1535
+ ISO27001: {
1536
+ fullName: "ISO/IEC 27001:2022 Information Security Management",
1537
+ focusAreas: [
1538
+ "Information security policies (Clause 5)",
1539
+ "Risk assessment and treatment (Clause 6)",
1540
+ "Information assets classification",
1541
+ "Access control (Annex A 5.15\u20135.18)",
1542
+ "Cryptography (Annex A 8.24)",
1543
+ "Supplier relationships (Annex A 5.19\u20135.22)"
1544
+ ],
1545
+ requiredSections: [
1546
+ "Scope and Context",
1547
+ "Information Assets",
1548
+ "Risk Assessment",
1549
+ "Access Control",
1550
+ "Cryptography and Data Protection",
1551
+ "Incident Management",
1552
+ "Business Continuity"
1553
+ ],
1554
+ sectionGuidance: "Structure the document around the ISO 27001:2022 Annex A controls. Map each capability to the relevant controls and identify statement of applicability (SoA) status."
1555
+ },
1556
+ GDPR: {
1557
+ fullName: "General Data Protection Regulation (GDPR)",
1558
+ focusAreas: [
1559
+ "Article 30 Records of Processing Activities (ROPA)",
1560
+ "Lawful basis for processing",
1561
+ "Data subject rights (Articles 15\u201322)",
1562
+ "Data retention and deletion",
1563
+ "Data transfers (Chapter V)",
1564
+ "Privacy by design and default (Article 25)"
1565
+ ],
1566
+ requiredSections: [
1567
+ "Records of Processing Activities (ROPA)",
1568
+ "Lawful Basis for Processing",
1569
+ "Data Subject Rights",
1570
+ "Data Retention Policy",
1571
+ "International Data Transfers",
1572
+ "Privacy by Design",
1573
+ "Data Breach Response"
1574
+ ],
1575
+ sectionGuidance: "Structure around Article 30 ROPA format. For each capability processing personal data: identify purpose, lawful basis, categories of data, retention period, and third-party processors."
1576
+ },
1577
+ SOC2: {
1578
+ fullName: "SOC 2 Type II (Trust Services Criteria)",
1579
+ focusAreas: [
1580
+ "Security (Common Criteria \u2014 CC)",
1581
+ "Availability (Availability Criteria \u2014 A)",
1582
+ "Confidentiality (Confidentiality Criteria \u2014 C)",
1583
+ "Processing Integrity (PI)",
1584
+ "Privacy (P)"
1585
+ ],
1586
+ requiredSections: [
1587
+ "System Description",
1588
+ "Security \u2014 Common Criteria",
1589
+ "Availability",
1590
+ "Confidentiality",
1591
+ "Change Management",
1592
+ "Monitoring",
1593
+ "Vendor Management"
1594
+ ],
1595
+ sectionGuidance: "Follow the AICPA Trust Services Criteria. For each section, identify relevant capabilities, current controls, and any gaps versus the criteria."
1596
+ },
1597
+ CUSTOM: {
1598
+ fullName: "Custom Compliance Framework",
1599
+ focusAreas: [
1600
+ "Scope and applicability",
1601
+ "Control requirements",
1602
+ "Risk assessment",
1603
+ "Monitoring and audit"
1604
+ ],
1605
+ requiredSections: [
1606
+ "Scope",
1607
+ "Controls Inventory",
1608
+ "Risk Assessment",
1609
+ "Gaps and Recommendations"
1610
+ ],
1611
+ sectionGuidance: "Generate a structured compliance document based on the codebase capabilities and general best practices."
1612
+ }
1613
+ };
1614
+ function buildCodebaseContext(blueprint, opts) {
1615
+ const lines = [
1616
+ `## Codebase Overview`,
1617
+ `Project: ${opts.projectName}`,
1618
+ `Total files: ${blueprint.stats.totalFiles}`,
1619
+ `Dependency edges: ${blueprint.stats.runtimeEdges}`
1620
+ ];
1621
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
1622
+ lines.push(``, `## Most-imported files (likely critical components)`);
1623
+ for (const f of topFiles) {
1624
+ lines.push(`- ${f.path} [${f.category ?? "unknown"}] \u2014 imported by ${f.importedByCount ?? 0} modules`);
1625
+ }
1626
+ return lines.join("\n");
1627
+ }
1628
+ function buildAmberContext(amber) {
1629
+ const lines = [`## AMBER Capability Registry`, amber.summary, ``];
1630
+ for (const cap of amber.capabilities.slice(0, 20)) {
1631
+ const tags = [cap.criticality, cap.lifecycle];
1632
+ if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
1633
+ if (cap.hasTests) tags.push("documented");
1634
+ lines.push(
1635
+ `- ${cap.name} (${cap.id}) [${tags.join(", ")}]` + (cap.description ? `: ${cap.description}` : "")
1636
+ );
1637
+ }
1638
+ return lines.join("\n");
1639
+ }
1640
+ function buildComplianceContext(ctx, framework) {
1641
+ const lines = [
1642
+ `## PRISM Compliance Analysis`,
1643
+ `Framework: ${ctx.framework}`,
1644
+ `Total capabilities: ${ctx.totalCapabilities}`,
1645
+ `Risk summary \u2014 High: ${ctx.riskSummary.high}, Medium: ${ctx.riskSummary.medium}, Low: ${ctx.riskSummary.low}`,
1646
+ ``,
1647
+ `### Critical / High Capabilities`
1648
+ ];
1649
+ for (const cap of ctx.criticalCapabilities.slice(0, 15)) {
1650
+ const tags = [cap.criticality];
1651
+ if (cap.accessControl) tags.push("access-control");
1652
+ if (cap.auditTrail) tags.push("audit-trail");
1653
+ if (cap.dataProcessing) tags.push("data-processing");
1654
+ if (!cap.testCoverage) tags.push("no-doc");
1655
+ if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
1656
+ lines.push(`- ${cap.name} (${cap.id}) [${tags.join(", ")}]`);
1657
+ }
1658
+ if (framework === "SOX" && ctx.sox) {
1659
+ lines.push(``, `### SOX: Financial capabilities: ${ctx.sox.financialCapabilities.join(", ") || "none identified"}`);
1660
+ lines.push(`SOX: Access-controlled: ${ctx.sox.accessControlled.join(", ") || "none identified"}`);
1661
+ }
1662
+ if (framework === "GDPR" && ctx.gdpr) {
1663
+ lines.push(``, `### GDPR: Data processing capabilities: ${ctx.gdpr.dataProcessingCapabilities.join(", ") || "none identified"}`);
1664
+ lines.push(`GDPR: Retention capabilities: ${ctx.gdpr.retentionCapabilities.join(", ") || "none identified"}`);
1665
+ }
1666
+ if (framework === "ISO27001" && ctx.iso27001) {
1667
+ lines.push(``, `### ISO27001: Information assets: ${ctx.iso27001.informationAssets.join(", ") || "none identified"}`);
1668
+ lines.push(`ISO27001: Security controls: ${ctx.iso27001.securityControls.join(", ") || "none identified"}`);
1669
+ }
1670
+ if (framework === "SOC2" && ctx.soc2) {
1671
+ lines.push(``, `### SOC2: Security capabilities: ${ctx.soc2.securityCapabilities.join(", ") || "none identified"}`);
1672
+ lines.push(`SOC2: Availability capabilities: ${ctx.soc2.availabilityCapabilities.join(", ") || "none identified"}`);
1673
+ }
1674
+ return lines.join("\n");
1675
+ }
1676
+ function buildSystemPrompt8(opts) {
1677
+ const toneMap = {
1678
+ formal: "You are a senior compliance officer writing a formal regulatory compliance document for external auditors and regulators.",
1679
+ technical: "You are a security-focused software architect writing a technical compliance assessment grounded in code-level evidence.",
1680
+ executive: "You are a Chief Compliance Officer writing a concise executive summary compliance report for board-level review."
1681
+ };
1682
+ return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
1683
+ }
1684
+ function buildUserPrompt8(opts) {
1685
+ const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
1686
+ const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
1687
+ const contextParts = [
1688
+ buildCodebaseContext(opts.blueprint, opts)
1689
+ ];
1690
+ if (opts.amberContext) contextParts.push(buildAmberContext(opts.amberContext));
1691
+ if (opts.complianceContext) contextParts.push(buildComplianceContext(opts.complianceContext, opts.framework));
1692
+ const org = opts.organizationName ? `Organization: ${opts.organizationName}` : "";
1693
+ const recsInstruction = opts.includeRecommendations ? 'Include a "recommendations" array with 3\u20137 actionable remediation items.' : 'Set "recommendations" to an empty array.';
1694
+ return [
1695
+ `Generate a ${frameworkName} compliance document for: ${opts.projectName}`,
1696
+ org,
1697
+ `Tone: ${opts.tone}`,
1698
+ ``,
1699
+ contextParts.join("\n\n"),
1700
+ ``,
1701
+ `## Framework: ${frameworkName}`,
1702
+ `Focus areas:`,
1703
+ spec.focusAreas.map((f) => `- ${f}`).join("\n"),
1704
+ ``,
1705
+ spec.sectionGuidance,
1706
+ ``,
1707
+ `Required sections: ${spec.requiredSections.join(", ")}`,
1708
+ ``,
1709
+ `## Output Format`,
1710
+ `Return a single JSON object with this exact shape:`,
1711
+ `{`,
1712
+ ` "documentTitle": "<formal document title>",`,
1713
+ ` "executiveSummary": "<3\u20135 sentences summarising compliance posture>",`,
1714
+ ` "confidentiality": "INTERNAL" | "CONFIDENTIAL" | "PUBLIC",`,
1715
+ ` "complianceGaps": ["<gap 1>", "<gap 2>"],`,
1716
+ ` "recommendations": ["<rec 1>", "<rec 2>"],`,
1717
+ ` "sections": [`,
1718
+ ` {`,
1719
+ ` "title": "<section title>",`,
1720
+ ` "content": "<substantive section prose, 2\u20134 paragraphs>",`,
1721
+ ` "capabilities": ["<capability id 1>"],`,
1722
+ ` "riskLevel": "high" | "medium" | "low",`,
1723
+ ` "status": "compliant" | "partial" | "gap"`,
1724
+ ` }`,
1725
+ ` ]`,
1726
+ `}`,
1727
+ ``,
1728
+ `Rules:`,
1729
+ `- Derive ALL content from the codebase context above \u2014 no invented facts.`,
1730
+ `- Each section MUST reference real capability IDs from the AMBER registry where available.`,
1731
+ `- complianceGaps: list specific gaps found (missing controls, undocumented capabilities, drift).`,
1732
+ recsInstruction,
1733
+ `- confidentiality: CONFIDENTIAL for SOX/ISO27001/SOC2, INTERNAL for GDPR/CUSTOM.`,
1734
+ `- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
1735
+ ].filter(Boolean).join("\n");
1736
+ }
1737
+ function buildFallback2(opts) {
1738
+ const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
1739
+ const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
1740
+ const totalCaps = opts.complianceContext?.totalCapabilities ?? opts.amberContext?.capabilities.length ?? 0;
1741
+ const highRisk = opts.complianceContext?.riskSummary.high ?? 0;
1742
+ const sections = spec.requiredSections.map((title) => ({
1743
+ title,
1744
+ content: `Assessment of ${title.toLowerCase()} for ${opts.projectName}. Based on Blueprint analysis, ${totalCaps} capabilities were identified for review under ${frameworkName}.`,
1745
+ capabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
1746
+ riskLevel: "medium",
1747
+ status: "partial"
1748
+ }));
1749
+ return {
1750
+ framework: opts.framework,
1751
+ documentTitle: `${opts.projectName} \u2014 ${frameworkName} Compliance Assessment`,
1752
+ executiveSummary: `This document presents the ${frameworkName} compliance assessment for ${opts.projectName}. Analysis identified ${totalCaps} business capabilities, of which ${highRisk} present elevated risk requiring remediation. The assessment covers key control domains and provides a structured gap analysis.`,
1753
+ sections,
1754
+ complianceGaps: [
1755
+ `${totalCaps - (opts.amberContext?.taggedFiles ?? 0)} files lack capability annotations \u2014 compliance traceability is incomplete.`,
1756
+ `${opts.amberContext?.driftedCapabilities ?? 0} capabilities have documentation drift \u2014 controls may be misrepresented.`
1757
+ ],
1758
+ recommendations: opts.includeRecommendations ? [
1759
+ "Ensure all critical capabilities have up-to-date @amber-doc annotations.",
1760
+ "Implement automated compliance scanning in CI/CD pipeline.",
1761
+ "Conduct quarterly capability review against framework requirements."
1762
+ ] : [],
1763
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1764
+ confidentiality: opts.framework === "GDPR" || opts.framework === "CUSTOM" ? "INTERNAL" : "CONFIDENTIAL"
1765
+ };
1766
+ }
1767
+ function parseLlmResponse2(raw, opts) {
1768
+ let text = raw.trim();
1769
+ if (text.startsWith("```")) {
1770
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
1771
+ }
1772
+ let parsed;
1773
+ try {
1774
+ parsed = JSON.parse(text);
1775
+ } catch {
1776
+ return null;
1777
+ }
1778
+ const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
1779
+ if (rawSections.length === 0) return null;
1780
+ const sections = rawSections.map((s) => {
1781
+ const sec = s ?? {};
1782
+ return {
1783
+ title: typeof sec.title === "string" ? sec.title : "Section",
1784
+ content: typeof sec.content === "string" ? sec.content : "",
1785
+ capabilities: Array.isArray(sec.capabilities) ? sec.capabilities.filter((c) => typeof c === "string") : [],
1786
+ ...typeof sec.riskLevel === "string" && ["high", "medium", "low"].includes(sec.riskLevel) ? { riskLevel: sec.riskLevel } : {},
1787
+ ...typeof sec.status === "string" && ["compliant", "partial", "gap"].includes(sec.status) ? { status: sec.status } : {}
1788
+ };
1789
+ });
1790
+ const confidentiality = parsed.confidentiality === "CONFIDENTIAL" ? "CONFIDENTIAL" : parsed.confidentiality === "PUBLIC" ? "PUBLIC" : "INTERNAL";
1791
+ return {
1792
+ framework: opts.framework,
1793
+ documentTitle: typeof parsed.documentTitle === "string" ? parsed.documentTitle : `${opts.projectName} \u2014 Compliance Assessment`,
1794
+ sections,
1795
+ executiveSummary: typeof parsed.executiveSummary === "string" ? parsed.executiveSummary : "",
1796
+ complianceGaps: Array.isArray(parsed.complianceGaps) ? parsed.complianceGaps.filter((g) => typeof g === "string") : [],
1797
+ recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations.filter((r) => typeof r === "string") : [],
1798
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1799
+ confidentiality
1800
+ };
1801
+ }
1802
+ async function generateComplianceDoc(opts) {
1803
+ const systemPrompt = buildSystemPrompt8(opts);
1804
+ const userPrompt = buildUserPrompt8(opts);
1805
+ try {
1806
+ const response = await opts.llm.complete({
1807
+ systemPrompt,
1808
+ messages: [{ role: "user", content: userPrompt }],
1809
+ maxTokens: 6e3
1810
+ });
1811
+ const parsed = parseLlmResponse2(response.content, opts);
1812
+ if (parsed) return parsed;
1813
+ } catch {
1814
+ }
1815
+ return buildFallback2(opts);
1816
+ }
1817
+
1818
+ // src/generators/generateADR.ts
1819
+ function buildCodebaseContext2(blueprint, projectName) {
1820
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
1821
+ const totalLoc = blueprint.files.reduce((sum, f) => sum + (f.lineCount ?? 0), 0);
1822
+ const lines = [
1823
+ `## Codebase Overview`,
1824
+ `Project: ${projectName}`,
1825
+ `Total files: ${blueprint.stats.totalFiles}`,
1826
+ `Total LOC: ${totalLoc.toLocaleString()}`,
1827
+ `Dependency edges: ${blueprint.stats.runtimeEdges}`,
1828
+ ``,
1829
+ `## Most-imported files (architectural hotspots)`
1830
+ ];
1831
+ for (const f of topFiles) {
1832
+ lines.push(`- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} modules, ${f.lineCount ?? 0} lines`);
1833
+ }
1834
+ if (blueprint.edges.length > 0) {
1835
+ const edgeSample = blueprint.edges.slice(0, 8);
1836
+ lines.push(``, `## Dependency sample`);
1837
+ for (const e of edgeSample) {
1838
+ lines.push(`- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`);
1839
+ }
1840
+ }
1841
+ return lines.join("\n");
1842
+ }
1843
+ function buildAdrContext(ctx, focus, selectedCycle, selectedCapability) {
1844
+ const lines = [`## PRISM Analysis`];
1845
+ if (ctx.coherenceScore !== null) {
1846
+ lines.push(`Architecture Coherence Score: ${ctx.coherenceScore}/100`);
1847
+ }
1848
+ if (ctx.architecturalPatterns.length > 0) {
1849
+ lines.push(`Detected patterns: ${ctx.architecturalPatterns.join(", ")}`);
1850
+ }
1851
+ if (ctx.topRisks.length > 0) {
1852
+ lines.push(``, `### Top Architectural Risks`);
1853
+ for (const r of ctx.topRisks.slice(0, 5)) {
1854
+ lines.push(`- ${r.name} (${r.capabilityId}): ${r.reason}`);
1855
+ }
1856
+ }
1857
+ if ((focus === "cycles" || focus === "overall") && ctx.importCycles.length > 0) {
1858
+ lines.push(``, `### Import Cycles (${ctx.importCycles.length} detected)`);
1859
+ const cyclesToShow = selectedCycle ? ctx.importCycles.filter((c) => c.files.some((f) => selectedCycle.includes(f))) : ctx.importCycles.slice(0, 5);
1860
+ for (const c of cyclesToShow) {
1861
+ lines.push(`- [${c.severity}] ${c.files.join(" \u2192 ")}`);
1862
+ }
1863
+ }
1864
+ if ((focus === "capabilities" || focus === "overall") && ctx.capabilityBoundaries.length > 0) {
1865
+ lines.push(``, `### Capability Boundaries (dependency edges)`);
1866
+ const boundaries = selectedCapability ? ctx.capabilityBoundaries.filter((b) => b.from === selectedCapability || b.to === selectedCapability) : ctx.capabilityBoundaries.slice(0, 10);
1867
+ for (const b of boundaries) {
1868
+ lines.push(`- ${b.from} \u2192 ${b.to} (${b.edgeCount} import edges)`);
1869
+ }
1870
+ }
1871
+ if ((focus === "dependencies" || focus === "overall") && ctx.highChurnFiles.length > 0) {
1872
+ lines.push(``, `### High-Churn Files`);
1873
+ for (const f of ctx.highChurnFiles.slice(0, 8)) {
1874
+ const caps = f.capabilities.length > 0 ? ` [${f.capabilities.join(", ")}]` : "";
1875
+ lines.push(`- ${f.path}${caps} \u2014 ${f.commits} commits`);
1876
+ }
1877
+ }
1878
+ if (ctx.orphanedFiles.length > 0) {
1879
+ lines.push(``, `### Orphaned Files (${ctx.orphanedFiles.length} untagged)`);
1880
+ for (const f of ctx.orphanedFiles.slice(0, 5)) {
1881
+ lines.push(`- ${f}`);
1882
+ }
1883
+ }
1884
+ return lines.join("\n");
1885
+ }
1886
+ function buildAmberContext2(amber) {
1887
+ const lines = [`## AMBER Capability Registry`, amber.summary, ``];
1888
+ for (const cap of amber.capabilities.slice(0, 15)) {
1889
+ const tags = [cap.criticality, cap.lifecycle];
1890
+ if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
1891
+ lines.push(
1892
+ `- ${cap.name} (${cap.id}) [${tags.join(", ")}]${cap.files.length > 0 ? ` \u2014 ${cap.files.length} files` : ""}`
1893
+ );
1894
+ }
1895
+ return lines.join("\n");
1896
+ }
1897
+ function buildSystemPrompt9() {
1898
+ return "You are a principal software architect with deep expertise in architecture decision records (ADRs) following the MADR (Markdown Architectural Decision Records) format. You analyse codebases and produce actionable, evidence-grounded ADRs. Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.";
1899
+ }
1900
+ function buildUserPrompt9(opts) {
1901
+ const focusDescriptions = {
1902
+ overall: "Generate a comprehensive set of ADRs covering the most significant architectural decisions evident from import cycles, capability boundaries, dependency patterns, and code structure.",
1903
+ cycles: "Focus on ADRs that address the import cycle patterns detected. Each cycle represents an implicit architectural decision \u2014 make those decisions explicit and propose resolutions.",
1904
+ capabilities: "Focus on ADRs about capability boundary decisions \u2014 how capabilities are decomposed, what depends on what, and whether the boundaries are intentional or accidental.",
1905
+ dependencies: "Focus on ADRs about dependency management \u2014 high-churn files, hotspots, fan-out patterns, and whether the dependency structure aligns with the intended architecture."
1906
+ };
1907
+ const parts = [
1908
+ `Generate 3\u20137 Architecture Decision Records (ADRs) for: ${opts.projectName}`,
1909
+ `Focus: ${opts.focus} \u2014 ${focusDescriptions[opts.focus]}`,
1910
+ ``,
1911
+ buildCodebaseContext2(opts.blueprint, opts.projectName)
1912
+ ];
1913
+ if (opts.amberContext) {
1914
+ parts.push(``, buildAmberContext2(opts.amberContext));
1915
+ }
1916
+ if (opts.adrContext) {
1917
+ parts.push(``, buildAdrContext(opts.adrContext, opts.focus, opts.selectedCycle, opts.selectedCapability));
1918
+ }
1919
+ if (opts.selectedCycle && opts.selectedCycle.length > 0) {
1920
+ parts.push(``, `## Selected Cycle for Focus`, opts.selectedCycle.join(" \u2192 "));
1921
+ }
1922
+ if (opts.selectedCapability) {
1923
+ parts.push(``, `## Selected Capability for Focus: ${opts.selectedCapability}`);
1924
+ }
1925
+ parts.push(
1926
+ ``,
1927
+ `## Output Format`,
1928
+ `Return a single JSON object:`,
1929
+ `{`,
1930
+ ` "summary": "<2\u20133 sentence overall architectural assessment>",`,
1931
+ ` "technicalDebt": "<1\u20132 sentence estimate of technical debt severity>",`,
1932
+ ` "adrs": [`,
1933
+ ` {`,
1934
+ ` "id": "ADR-001",`,
1935
+ ` "title": "<short imperative title>",`,
1936
+ ` "date": "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}",`,
1937
+ ` "status": "Proposed",`,
1938
+ ` "context": "<what is the situation and why does this decision need to be made? 2\u20134 sentences>",`,
1939
+ ` "decision": "<what was decided and why? 2\u20134 sentences with rationale>",`,
1940
+ ` "consequences": {`,
1941
+ ` "positive": ["<benefit 1>", "<benefit 2>"],`,
1942
+ ` "negative": ["<drawback 1>"],`,
1943
+ ` "neutral": ["<neutral fact>"]`,
1944
+ ` },`,
1945
+ ` "alternatives": ["<alternative option 1>", "<alternative option 2>"],`,
1946
+ ` "relatedCapabilities": ["<capability-id-1>"],`,
1947
+ ` "relatedFiles": ["<file/path.ts>"]`,
1948
+ ` }`,
1949
+ ` ]`,
1950
+ `}`,
1951
+ ``,
1952
+ `Rules:`,
1953
+ `- IDs: ADR-001, ADR-002, \u2026 in order.`,
1954
+ `- Derive ALL content from the codebase data above \u2014 no invented facts.`,
1955
+ `- Each ADR MUST address a real pattern visible in the data (cycle, boundary, dependency).`,
1956
+ `- relatedCapabilities: reference real capability IDs from AMBER registry when available.`,
1957
+ `- relatedFiles: reference real file paths from the blueprint.`,
1958
+ `- status is always "Proposed" for new ADRs.`,
1959
+ `- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
1960
+ );
1961
+ return parts.join("\n");
1962
+ }
1963
+ function buildFallback3(opts) {
1964
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1965
+ const adrs = [];
1966
+ if (opts.focus === "cycles" || opts.focus === "overall") {
1967
+ const cycleFiles = opts.adrContext?.importCycles[0]?.files ?? opts.selectedCycle ?? [];
1968
+ adrs.push({
1969
+ id: "ADR-001",
1970
+ title: "Introduce Module Boundary Abstractions to Break Import Cycles",
1971
+ date: today,
1972
+ status: "Proposed",
1973
+ context: `${opts.projectName} contains ${opts.adrContext?.importCycles.length ?? "multiple"} detected import cycles. Circular dependencies create tight coupling that makes independent testing and deployment difficult.`,
1974
+ decision: "Introduce shared interface/abstraction modules at cycle breakpoints to invert dependencies. Files that create cycles should depend on abstractions, not on each other directly.",
1975
+ consequences: {
1976
+ positive: ["Enables independent testing of cyclic modules", "Reduces coupling between layers"],
1977
+ negative: ["Requires refactoring existing imports", "Short-term increase in file count"],
1978
+ neutral: ["Aligns with Dependency Inversion Principle"]
1979
+ },
1980
+ alternatives: ["Extract shared state to a separate module", "Use event-driven communication between modules"],
1981
+ relatedCapabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
1982
+ relatedFiles: cycleFiles.slice(0, 3)
1983
+ });
1984
+ }
1985
+ if (opts.focus === "capabilities" || opts.focus === "overall") {
1986
+ adrs.push({
1987
+ id: `ADR-00${adrs.length + 1}`,
1988
+ title: "Enforce Explicit Capability Boundaries with Barrel Exports",
1989
+ date: today,
1990
+ status: "Proposed",
1991
+ context: `Capability boundaries in ${opts.projectName} are implicit \u2014 cross-capability imports create unintended dependencies. The AMBER registry identifies capabilities but import boundaries don't enforce them.`,
1992
+ decision: "Each capability should expose a single barrel index file. External modules import only from the barrel, never from internal implementation files.",
1993
+ consequences: {
1994
+ positive: ["Capability-level API surfaces become explicit", "Refactoring internals doesn't break consumers"],
1995
+ negative: ["Requires creating barrel files for each capability", "Barrel files need maintenance"],
1996
+ neutral: ["Standard pattern in large TypeScript codebases"]
1997
+ },
1998
+ alternatives: ["Use ESLint import boundaries rules", "Monorepo packages per capability"],
1999
+ relatedCapabilities: opts.amberContext?.capabilities.slice(0, 3).map((c) => c.id) ?? [],
2000
+ relatedFiles: []
2001
+ });
2002
+ }
2003
+ adrs.push({
2004
+ id: `ADR-00${adrs.length + 1}`,
2005
+ title: "Establish Architectural Fitness Functions for Ongoing Compliance",
2006
+ date: today,
2007
+ status: "Proposed",
2008
+ context: `The ${opts.projectName} codebase currently lacks automated enforcement of architectural constraints. Without fitness functions, architectural drift accumulates silently.`,
2009
+ decision: "Integrate PRISM / prism0x2A scans into CI to track coherence score, import cycles, and capability drift. Gate merges on cycle count not increasing.",
2010
+ consequences: {
2011
+ positive: ["Architectural health is continuously measured", "Teams get early warning of degradation"],
2012
+ negative: ["CI time increases slightly", "Requires PRISM setup in CI environment"],
2013
+ neutral: ["Shift from reactive to proactive architecture governance"]
2014
+ },
2015
+ alternatives: ["Manual architecture review cadence", "Weekly automated reports only"],
2016
+ relatedCapabilities: [],
2017
+ relatedFiles: []
2018
+ });
2019
+ return {
2020
+ adrs,
2021
+ summary: `${opts.projectName} has ${opts.blueprint.stats.totalFiles} files with ${opts.blueprint.stats.runtimeEdges} dependency edges. ${opts.adrContext?.importCycles.length ?? 0} import cycles and ${opts.adrContext?.capabilityBoundaries.length ?? 0} cross-capability dependency edges indicate structural decisions that should be made explicit via ADRs.`,
2022
+ technicalDebt: "Moderate-to-high architectural debt evident from cycle patterns and implicit capability boundaries. Estimated 3\u20136 engineering weeks to implement the proposed decisions."
2023
+ };
2024
+ }
2025
+ function parseLlmResponse3(raw) {
2026
+ let text = raw.trim();
2027
+ if (text.startsWith("```")) {
2028
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2029
+ }
2030
+ let parsed;
2031
+ try {
2032
+ parsed = JSON.parse(text);
2033
+ } catch {
2034
+ return null;
2035
+ }
2036
+ const rawAdrs = Array.isArray(parsed.adrs) ? parsed.adrs : [];
2037
+ if (rawAdrs.length === 0) return null;
2038
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2039
+ const adrs = rawAdrs.map((a, idx) => {
2040
+ const adr = a ?? {};
2041
+ const consequences = typeof adr.consequences === "object" && adr.consequences !== null ? adr.consequences : {};
2042
+ const status = typeof adr.status === "string" && ["Proposed", "Accepted", "Deprecated", "Superseded"].includes(adr.status) ? adr.status : "Proposed";
2043
+ return {
2044
+ id: typeof adr.id === "string" ? adr.id : `ADR-${String(idx + 1).padStart(3, "0")}`,
2045
+ title: typeof adr.title === "string" ? adr.title : `Decision ${idx + 1}`,
2046
+ date: typeof adr.date === "string" ? adr.date : today,
2047
+ status,
2048
+ context: typeof adr.context === "string" ? adr.context : "",
2049
+ decision: typeof adr.decision === "string" ? adr.decision : "",
2050
+ consequences: {
2051
+ positive: Array.isArray(consequences.positive) ? consequences.positive.filter((s) => typeof s === "string") : [],
2052
+ negative: Array.isArray(consequences.negative) ? consequences.negative.filter((s) => typeof s === "string") : [],
2053
+ neutral: Array.isArray(consequences.neutral) ? consequences.neutral.filter((s) => typeof s === "string") : []
2054
+ },
2055
+ alternatives: Array.isArray(adr.alternatives) ? adr.alternatives.filter((s) => typeof s === "string") : [],
2056
+ relatedCapabilities: Array.isArray(adr.relatedCapabilities) ? adr.relatedCapabilities.filter((s) => typeof s === "string") : [],
2057
+ relatedFiles: Array.isArray(adr.relatedFiles) ? adr.relatedFiles.filter((s) => typeof s === "string") : []
2058
+ };
2059
+ });
2060
+ return {
2061
+ adrs,
2062
+ summary: typeof parsed.summary === "string" ? parsed.summary : "",
2063
+ technicalDebt: typeof parsed.technicalDebt === "string" ? parsed.technicalDebt : ""
2064
+ };
2065
+ }
2066
+ async function generateADR(opts) {
2067
+ try {
2068
+ const response = await opts.llm.complete({
2069
+ systemPrompt: buildSystemPrompt9(),
2070
+ messages: [{ role: "user", content: buildUserPrompt9(opts) }],
2071
+ maxTokens: 6e3
2072
+ });
2073
+ const parsed = parseLlmResponse3(response.content);
2074
+ if (parsed) return parsed;
2075
+ } catch {
2076
+ }
2077
+ return buildFallback3(opts);
2078
+ }
2079
+
2080
+ // src/generators/generateSprintRetro.ts
2081
+ function classifyCommit(subject) {
2082
+ const s = subject.toLowerCase();
2083
+ if (s.startsWith("feat") || s.includes("add ") || s.includes("implement") || s.includes("new ")) return "feature";
2084
+ if (s.startsWith("fix") || s.includes("bug") || s.includes("patch") || s.includes("hotfix")) return "fix";
2085
+ if (s.startsWith("refactor") || s.startsWith("refact") || s.includes("rewrite") || s.includes("restructure")) return "refactor";
2086
+ return "chore";
2087
+ }
2088
+ function buildContext2(opts) {
2089
+ const lines = [
2090
+ `## Sprint Context`,
2091
+ `Project: ${opts.projectName}`,
2092
+ `Sprint: ${opts.sprintName ?? "Current Sprint"}`,
2093
+ `Team size: ${opts.teamSize ?? "unknown"}`,
2094
+ `Tone: ${opts.tone}`,
2095
+ ``
2096
+ ];
2097
+ if (opts.gitContext) {
2098
+ const { commits, filesChanged, fromRef, toRef } = opts.gitContext;
2099
+ lines.push(
2100
+ `## Git Activity (${fromRef} \u2192 ${toRef})`,
2101
+ `Commits: ${commits.length}`,
2102
+ `Files changed: ${filesChanged}`,
2103
+ ``,
2104
+ `### Commits`,
2105
+ ...commits.slice(0, 30).map(
2106
+ (c) => `- [${classifyCommit(c.subject)}] ${c.subject}${c.author ? ` \u2014 ${c.author}` : ""}`
2107
+ ),
2108
+ ``
2109
+ );
2110
+ }
2111
+ if (opts.scoreBefore !== void 0 || opts.scoreAfter !== void 0) {
2112
+ lines.push(
2113
+ `## Architecture Health`,
2114
+ `Score at sprint start: ${opts.scoreBefore ?? "unknown"}`,
2115
+ `Score at sprint end: ${opts.scoreAfter ?? "unknown"}`,
2116
+ ``
2117
+ );
2118
+ }
2119
+ if (opts.amberContext) {
2120
+ const { capabilities, driftedCapabilities, summary } = opts.amberContext;
2121
+ lines.push(
2122
+ `## AMBER Capability Layer`,
2123
+ summary,
2124
+ `Drifted capabilities: ${driftedCapabilities}`,
2125
+ ``,
2126
+ `### Capabilities`,
2127
+ ...capabilities.slice(0, 15).map(
2128
+ (c) => `- ${c.name} [${c.criticality}/${c.lifecycle}]${c.driftCount > 0 ? ` \u26A0 drift: ${c.driftCount} files` : ""}`
2129
+ ),
2130
+ ``
2131
+ );
2132
+ }
2133
+ if (opts.greenContext) {
2134
+ lines.push(
2135
+ `## GREEN Cross-Layer Insights`,
2136
+ opts.greenContext.summary,
2137
+ ``
2138
+ );
2139
+ if (opts.greenContext.topRisks.length > 0) {
2140
+ lines.push(
2141
+ `### Top Risks`,
2142
+ ...opts.greenContext.topRisks.slice(0, 5).map(
2143
+ (r) => `- ${r.name}: ${r.reason} (impact: ${r.impact}/10)`
2144
+ ),
2145
+ ``
2146
+ );
2147
+ }
2148
+ }
2149
+ return lines.join("\n");
2150
+ }
2151
+ function buildSystemPrompt10(opts) {
2152
+ const toneMap = {
2153
+ professional: "You are a senior engineering manager facilitating a data-driven sprint retrospective. Be precise and actionable.",
2154
+ casual: "You are a friendly scrum master running a fun and honest retrospective. Keep it light but insightful.",
2155
+ "team-friendly": "You are a team lead who wants everyone to feel heard. Balance honesty with encouragement."
2156
+ };
2157
+ return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
2158
+ }
2159
+ function buildUserPrompt10(opts) {
2160
+ const context = buildContext2(opts);
2161
+ const sprintName = opts.sprintName ?? "Current Sprint";
2162
+ return [
2163
+ `Generate a sprint retrospective for: ${opts.projectName} \u2014 ${sprintName}`,
2164
+ ``,
2165
+ context,
2166
+ ``,
2167
+ `## Output Format`,
2168
+ `Return a single JSON object with this exact shape:`,
2169
+ `{`,
2170
+ ` "sprintName": "${sprintName}",`,
2171
+ ` "period": "<date range or sprint identifier>",`,
2172
+ ` "summary": "<2-3 sentence executive summary of the sprint>",`,
2173
+ ` "delivered": [`,
2174
+ ` { "item": "<what was built>", "capability": "<AMBER capability id if applicable>", "type": "feature|fix|refactor|chore" }`,
2175
+ ` ],`,
2176
+ ` "healthDelta": {`,
2177
+ ` "scoreBefore": <number or null>,`,
2178
+ ` "scoreAfter": <number or null>,`,
2179
+ ` "delta": <number or null>,`,
2180
+ ` "verdict": "improved|stable|degraded",`,
2181
+ ` "degradedCapabilities": ["<capability name>"]`,
2182
+ ` },`,
2183
+ ` "wentWell": ["<positive thing 1>", ...],`,
2184
+ ` "improvements": ["<improvement needed 1>", ...],`,
2185
+ ` "puzzles": ["<question or confusion 1>", ...],`,
2186
+ ` "actions": ["<concrete action item 1>", ...],`,
2187
+ ` "debtIncurred": ["<technical debt item 1>", ...],`,
2188
+ ` "slideTitle": "<1 sentence sprint summary for a slide>",`,
2189
+ ` "slidePoints": ["<key point 1>", "<key point 2>", "<key point 3>"]`,
2190
+ `}`,
2191
+ ``,
2192
+ `Rules:`,
2193
+ `- delivered: derive from git commits, 5\u201315 items`,
2194
+ `- wentWell: 3\u20135 genuine positives from the data`,
2195
+ `- improvements: 3\u20135 concrete improvements (not vague)`,
2196
+ `- puzzles: 2\u20134 things that were unclear or confusing this sprint`,
2197
+ `- actions: 3\u20135 specific, assigned-sounding action items`,
2198
+ `- debtIncurred: 1\u20134 technical debt items visible from the commits/drift`,
2199
+ `- slidePoints: exactly 3 bullet strings \u2264 80 characters`,
2200
+ `- Derive everything from the context above \u2014 no invented facts`,
2201
+ `- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
2202
+ ].join("\n");
2203
+ }
2204
+ function computeHealthDelta(scoreBefore, scoreAfter, amberContext) {
2205
+ const before = scoreBefore ?? null;
2206
+ const after = scoreAfter ?? null;
2207
+ const delta = before !== null && after !== null ? after - before : null;
2208
+ let verdict = "stable";
2209
+ if (delta !== null) {
2210
+ if (delta > 2) verdict = "improved";
2211
+ else if (delta < -2) verdict = "degraded";
2212
+ }
2213
+ const degradedCapabilities = amberContext ? amberContext.capabilities.filter((c) => c.driftCount > 0 && (c.criticality === "critical" || c.criticality === "high")).map((c) => c.name) : [];
2214
+ return { scoreBefore: before, scoreAfter: after, delta, verdict, degradedCapabilities };
2215
+ }
2216
+ function buildFallback4(opts) {
2217
+ const sprintName = opts.sprintName ?? "Current Sprint";
2218
+ const commits = opts.gitContext?.commits ?? [];
2219
+ const healthDelta = computeHealthDelta(opts.scoreBefore, opts.scoreAfter, opts.amberContext);
2220
+ const delivered = commits.slice(0, 10).map((c) => ({
2221
+ item: c.subject,
2222
+ type: classifyCommit(c.subject)
2223
+ }));
2224
+ return {
2225
+ sprintName,
2226
+ period: opts.gitContext ? `${opts.gitContext.fromRef} \u2192 ${opts.gitContext.toRef}` : "This sprint",
2227
+ summary: `${opts.projectName} completed ${commits.length} commits this sprint across ${opts.gitContext?.filesChanged ?? 0} files.`,
2228
+ delivered,
2229
+ healthDelta,
2230
+ wentWell: [
2231
+ `${commits.filter((c) => classifyCommit(c.subject) === "feature").length} features shipped`,
2232
+ `${commits.filter((c) => classifyCommit(c.subject) === "fix").length} bugs fixed`,
2233
+ "Team maintained delivery cadence"
2234
+ ],
2235
+ improvements: [
2236
+ "Reduce WIP by completing in-flight work before starting new",
2237
+ "Improve commit message clarity",
2238
+ "Address documentation drift in critical capabilities"
2239
+ ],
2240
+ puzzles: [
2241
+ "Unclear ownership for cross-capability changes",
2242
+ "Test coverage strategy for new features"
2243
+ ],
2244
+ actions: [
2245
+ "Schedule architecture review for flagged capabilities",
2246
+ "Set up automated coherence score tracking in CI",
2247
+ "Document top 3 architectural decisions made this sprint"
2248
+ ],
2249
+ debtIncurred: healthDelta.degradedCapabilities.length > 0 ? healthDelta.degradedCapabilities.map((c) => `Documentation drift in ${c}`) : ["Review and address any undocumented capability changes"],
2250
+ slideTitle: `${sprintName}: ${commits.length} commits, ${opts.gitContext?.filesChanged ?? 0} files changed`,
2251
+ slidePoints: [
2252
+ `${commits.length} commits across ${opts.gitContext?.filesChanged ?? 0} files`,
2253
+ `Architecture health: ${healthDelta.verdict}`,
2254
+ `${healthDelta.degradedCapabilities.length} capabilities need attention`
2255
+ ]
2256
+ };
2257
+ }
2258
+ function parseLlmResponse4(raw, opts) {
2259
+ let text = raw.trim();
2260
+ if (text.startsWith("```")) {
2261
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2262
+ }
2263
+ let parsed;
2264
+ try {
2265
+ parsed = JSON.parse(text);
2266
+ } catch {
2267
+ return null;
2268
+ }
2269
+ const sprintName = typeof parsed.sprintName === "string" ? parsed.sprintName : opts.sprintName ?? "Current Sprint";
2270
+ const period = typeof parsed.period === "string" ? parsed.period : "";
2271
+ const summary = typeof parsed.summary === "string" ? parsed.summary : "";
2272
+ const rawDelivered = Array.isArray(parsed.delivered) ? parsed.delivered : [];
2273
+ const delivered = rawDelivered.filter((d) => typeof d.item === "string").map((d) => {
2274
+ const item = d;
2275
+ const type = ["feature", "fix", "refactor", "chore"].includes(item.type) ? item.type : "chore";
2276
+ return {
2277
+ item: item.item,
2278
+ ...typeof item.capability === "string" ? { capability: item.capability } : {},
2279
+ type
2280
+ };
2281
+ });
2282
+ const hd = parsed.healthDelta ?? {};
2283
+ const scoreBeforeRaw = typeof hd.scoreBefore === "number" ? hd.scoreBefore : null;
2284
+ const scoreAfterRaw = typeof hd.scoreAfter === "number" ? hd.scoreAfter : null;
2285
+ const deltaRaw = typeof hd.delta === "number" ? hd.delta : null;
2286
+ const verdictRaw = ["improved", "stable", "degraded"].includes(hd.verdict) ? hd.verdict : "stable";
2287
+ const degradedCaps = Array.isArray(hd.degradedCapabilities) ? hd.degradedCapabilities.filter((x) => typeof x === "string") : [];
2288
+ const toStringArray = (key) => {
2289
+ const val = parsed[key];
2290
+ if (!Array.isArray(val)) return [];
2291
+ return val.filter((x) => typeof x === "string");
2292
+ };
2293
+ if (!summary && delivered.length === 0) return null;
2294
+ return {
2295
+ sprintName,
2296
+ period,
2297
+ summary,
2298
+ delivered,
2299
+ healthDelta: {
2300
+ scoreBefore: scoreBeforeRaw,
2301
+ scoreAfter: scoreAfterRaw,
2302
+ delta: deltaRaw,
2303
+ verdict: verdictRaw,
2304
+ degradedCapabilities: degradedCaps
2305
+ },
2306
+ wentWell: toStringArray("wentWell"),
2307
+ improvements: toStringArray("improvements"),
2308
+ puzzles: toStringArray("puzzles"),
2309
+ actions: toStringArray("actions"),
2310
+ debtIncurred: toStringArray("debtIncurred"),
2311
+ slideTitle: typeof parsed.slideTitle === "string" ? parsed.slideTitle : sprintName,
2312
+ slidePoints: toStringArray("slidePoints").slice(0, 3)
2313
+ };
2314
+ }
2315
+ async function generateSprintRetro(opts) {
2316
+ try {
2317
+ const response = await opts.llm.complete({
2318
+ systemPrompt: buildSystemPrompt10(opts),
2319
+ messages: [{ role: "user", content: buildUserPrompt10(opts) }],
2320
+ maxTokens: 4096
2321
+ });
2322
+ const parsed = parseLlmResponse4(response.content, opts);
2323
+ if (parsed) return parsed;
2324
+ } catch {
2325
+ }
2326
+ return buildFallback4(opts);
2327
+ }
2328
+
2329
+ // src/generators/generateNewsletter.ts
2330
+ function buildContext3(opts) {
2331
+ const { digestContext: d, amberContext, projectName, teamName, targetAudience, tone } = opts;
2332
+ const lines = [
2333
+ `## Newsletter Context`,
2334
+ `Project: ${projectName}`,
2335
+ `Team: ${teamName ?? "Engineering"}`,
2336
+ `Target audience: ${targetAudience}`,
2337
+ `Tone: ${tone}`,
2338
+ ``,
2339
+ `## Weekly Digest \u2014 ${d.period}`,
2340
+ `Commits: ${d.commitCount}`,
2341
+ `Files changed: ${d.filesChanged}`,
2342
+ `Coherence grade: ${d.grade ?? "N/A"}`,
2343
+ `Score start: ${d.scoreStart ?? "N/A"}`,
2344
+ `Score end: ${d.scoreEnd ?? "N/A"}`,
2345
+ `Score delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"}`,
2346
+ `Health summary: ${d.healthSummary}`,
2347
+ ``
2348
+ ];
2349
+ if (d.topCommits.length > 0) {
2350
+ lines.push(`### Top commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
2351
+ }
2352
+ if (d.newDrifts.length > 0) {
2353
+ lines.push(`### New capability drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
2354
+ }
2355
+ if (d.resolvedDrifts.length > 0) {
2356
+ lines.push(`### Resolved drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
2357
+ }
2358
+ if (d.newRisks.length > 0) {
2359
+ lines.push(`### New risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
2360
+ }
2361
+ if (amberContext) {
2362
+ lines.push(`## AMBER Capability Summary`, amberContext.summary, ``);
2363
+ }
2364
+ return lines.join("\n");
2365
+ }
2366
+ function buildSystemPrompt11(opts) {
2367
+ const audienceMap = {
2368
+ "product-team": "You are writing a newsletter for the product engineering team \u2014 technical details are welcome.",
2369
+ management: "You are writing a newsletter for engineering managers \u2014 focus on velocity, quality, and risk.",
2370
+ stakeholders: "You are writing a newsletter for business stakeholders \u2014 translate technical work into business outcomes.",
2371
+ "all-hands": "You are writing a company-wide newsletter \u2014 accessible to everyone, inspiring, and concise."
2372
+ };
2373
+ const toneMap = {
2374
+ professional: "Use a professional, data-driven tone.",
2375
+ casual: "Use a friendly, conversational tone \u2014 contractions welcome.",
2376
+ accessible: "Use zero technical jargon. If you must mention a technical concept, define it in plain language immediately."
2377
+ };
2378
+ return [
2379
+ audienceMap[opts.targetAudience],
2380
+ toneMap[opts.tone],
2381
+ "Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object."
2382
+ ].join(" ");
2383
+ }
2384
+ function buildUserPrompt11(opts) {
2385
+ const context = buildContext3(opts);
2386
+ const { projectName, teamName, targetAudience, tone, includeMetrics } = opts;
2387
+ return [
2388
+ `Generate a weekly engineering newsletter for: ${projectName}`,
2389
+ `Team: ${teamName ?? "Engineering"} | Audience: ${targetAudience} | Tone: ${tone}`,
2390
+ `Include metrics: ${includeMetrics}`,
2391
+ ``,
2392
+ context,
2393
+ ``,
2394
+ `## Output Format`,
2395
+ `Return a single JSON object with this exact shape:`,
2396
+ `{`,
2397
+ ` "subject": "<email subject line, \u226460 chars>",`,
2398
+ ` "preview": "<email preview text, \u226450 chars>",`,
2399
+ ` "greeting": "<opening greeting>",`,
2400
+ ` "headline": "<1-sentence hook that opens the newsletter>",`,
2401
+ ` "sections": [`,
2402
+ ` { "heading": "<section heading>", "body": "<2-4 sentences>", "type": "highlight|metrics|risk|shoutout|upcoming" }`,
2403
+ ` ],`,
2404
+ ` "metrics": [`,
2405
+ ` { "label": "<metric name>", "value": "<value>", "trend": "up|down|stable" }`,
2406
+ ` ],`,
2407
+ ` "closing": "<1-2 sentence closing>",`,
2408
+ ` "unsubscribeNote": "Reply STOP to unsubscribe from this digest.",`,
2409
+ ` "slackVersion": "<Slack mrkdwn formatted version of the newsletter>",`,
2410
+ ` "teamsVersion": "<Microsoft Teams Adaptive Card JSON as a string>",`,
2411
+ ` "htmlVersion": "<complete HTML email with inline styles, dark-mode friendly>"`,
2412
+ `}`,
2413
+ ``,
2414
+ `Rules:`,
2415
+ `- sections: 3\u20135 sections covering the highlights`,
2416
+ `- metrics: ${includeMetrics ? "include 3\u20135 key metrics from the digest" : "return empty array []"}`,
2417
+ `- slackVersion: use *bold*, _italic_, \`code\`, > blockquote, \u2022 bullets (mrkdwn)`,
2418
+ `- teamsVersion: valid Teams Adaptive Card JSON with TextBlock, FactSet elements`,
2419
+ `- htmlVersion: full HTML with <html><head><body>, inline CSS, no external resources`,
2420
+ `- subject: must reference the week/project and be compelling`,
2421
+ `- Derive all content from the digest context above \u2014 no invented facts`,
2422
+ `- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
2423
+ ].join("\n");
2424
+ }
2425
+ function buildHtmlEmail(opts, sections, metrics) {
2426
+ const { projectName, teamName, digestContext: d } = opts;
2427
+ const sectionHtml = sections.map((s) => `
2428
+ <tr><td style="padding:20px 32px 0">
2429
+ <h2 style="margin:0 0 8px;font-size:16px;color:#e2e8f0">${s.heading}</h2>
2430
+ <p style="margin:0;color:#94a3b8;font-size:14px;line-height:1.6">${s.body}</p>
2431
+ </td></tr>`).join("");
2432
+ const metricsHtml = metrics.length > 0 ? `
2433
+ <tr><td style="padding:20px 32px 0">
2434
+ <table width="100%" cellpadding="0" cellspacing="0" border="0">
2435
+ <tr>${metrics.map((m) => `
2436
+ <td style="text-align:center;padding:12px;background:#1e293b;border-radius:8px;margin:4px">
2437
+ <div style="font-size:22px;font-weight:700;color:${m.trend === "up" ? "#34d399" : m.trend === "down" ? "#f87171" : "#94a3b8"}">${m.value}</div>
2438
+ <div style="font-size:12px;color:#64748b;margin-top:4px">${m.label}</div>
2439
+ </td>`).join("<td style='width:8px'></td>")}</tr>
2440
+ </table>
2441
+ </td></tr>` : "";
2442
+ return `<!DOCTYPE html>
2443
+ <html lang="en">
2444
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${projectName} \u2014 Weekly Digest</title></head>
2445
+ <body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
2446
+ <table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
2447
+ <tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
2448
+ <h1 style="margin:0;font-size:24px;font-weight:700;color:#f1f5f9">${projectName}</h1>
2449
+ <p style="margin:4px 0 0;color:#64748b;font-size:14px">${teamName ?? "Engineering"} \xB7 ${d.period}</p>
2450
+ </td></tr>
2451
+ ${sectionHtml}
2452
+ ${metricsHtml}
2453
+ <tr><td style="padding:24px 32px;border-top:1px solid #1e293b;margin-top:24px">
2454
+ <p style="margin:0;color:#475569;font-size:12px">Generated by forge0x2B \xB7 Reply STOP to unsubscribe.</p>
2455
+ </td></tr>
2456
+ </table>
2457
+ </body>
2458
+ </html>`;
2459
+ }
2460
+ function buildSlackVersion(opts, sections, metrics) {
2461
+ const { projectName, digestContext: d } = opts;
2462
+ const lines = [
2463
+ `*${projectName} \u2014 Weekly Engineering Digest*`,
2464
+ `_${d.period}_`,
2465
+ ``,
2466
+ `*Health:* ${d.healthSummary}`,
2467
+ ``,
2468
+ ...sections.map((s) => [`*${s.heading}*`, s.body, ""].join("\n"))
2469
+ ];
2470
+ if (metrics.length > 0) {
2471
+ lines.push(
2472
+ `*Key Metrics*`,
2473
+ ...metrics.map((m) => `\u2022 *${m.label}:* ${m.value} ${m.trend === "up" ? "\u2191" : m.trend === "down" ? "\u2193" : "\u2192"}`),
2474
+ ``
2475
+ );
2476
+ }
2477
+ lines.push(`_Generated by forge0x2B_`);
2478
+ return lines.join("\n");
2479
+ }
2480
+ function buildTeamsVersion(opts, sections) {
2481
+ const { projectName, digestContext: d } = opts;
2482
+ const card = {
2483
+ type: "AdaptiveCard",
2484
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
2485
+ version: "1.4",
2486
+ body: [
2487
+ {
2488
+ type: "TextBlock",
2489
+ text: `${projectName} \u2014 Weekly Engineering Digest`,
2490
+ weight: "Bolder",
2491
+ size: "Large",
2492
+ color: "Accent"
2493
+ },
2494
+ {
2495
+ type: "TextBlock",
2496
+ text: d.period,
2497
+ isSubtle: true,
2498
+ spacing: "None"
2499
+ },
2500
+ {
2501
+ type: "TextBlock",
2502
+ text: d.healthSummary,
2503
+ wrap: true,
2504
+ spacing: "Medium"
2505
+ },
2506
+ ...sections.slice(0, 3).map((s) => ({
2507
+ type: "Container",
2508
+ spacing: "Medium",
2509
+ items: [
2510
+ { type: "TextBlock", text: s.heading, weight: "Bolder", wrap: true },
2511
+ { type: "TextBlock", text: s.body, wrap: true, isSubtle: true }
2512
+ ]
2513
+ })),
2514
+ {
2515
+ type: "TextBlock",
2516
+ text: "Generated by forge0x2B",
2517
+ isSubtle: true,
2518
+ size: "Small",
2519
+ spacing: "Large"
2520
+ }
2521
+ ]
2522
+ };
2523
+ return JSON.stringify(card);
2524
+ }
2525
+ function buildFallback5(opts) {
2526
+ const { projectName, teamName, digestContext: d, includeMetrics } = opts;
2527
+ const sections = [
2528
+ {
2529
+ heading: "This Week in Engineering",
2530
+ body: `The team shipped ${d.commitCount} commits across ${d.filesChanged} files during ${d.period}. ${d.healthSummary}`,
2531
+ type: "highlight"
2532
+ }
2533
+ ];
2534
+ if (d.topCommits.length > 0) {
2535
+ sections.push({
2536
+ heading: "Highlights",
2537
+ body: d.topCommits.slice(0, 3).join(". ") + ".",
2538
+ type: "highlight"
2539
+ });
2540
+ }
2541
+ if (d.newRisks.length > 0) {
2542
+ sections.push({
2543
+ heading: "On Our Radar",
2544
+ body: `We identified ${d.newRisks.length} new risk${d.newRisks.length > 1 ? "s" : ""} this week: ${d.newRisks.slice(0, 2).join(", ")}.`,
2545
+ type: "risk"
2546
+ });
2547
+ }
2548
+ const metrics = includeMetrics ? [
2549
+ { label: "Commits", value: String(d.commitCount), trend: "stable" },
2550
+ { label: "Files Changed", value: String(d.filesChanged), trend: "stable" },
2551
+ ...d.grade ? [{ label: "Architecture Grade", value: d.grade, trend: d.scoreDelta !== null && d.scoreDelta > 0 ? "up" : d.scoreDelta !== null && d.scoreDelta < 0 ? "down" : "stable" }] : []
2552
+ ] : [];
2553
+ return {
2554
+ subject: `${projectName} Engineering Digest \u2014 ${d.period}`,
2555
+ preview: `${d.commitCount} commits \xB7 ${d.healthSummary.slice(0, 40)}`,
2556
+ greeting: `Hello ${teamName ?? "team"},`,
2557
+ headline: `Here's what the engineering team shipped this week.`,
2558
+ sections,
2559
+ metrics,
2560
+ closing: `Thanks for reading. See you next week!`,
2561
+ unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
2562
+ slackVersion: buildSlackVersion(opts, sections, metrics),
2563
+ teamsVersion: buildTeamsVersion(opts, sections),
2564
+ htmlVersion: buildHtmlEmail(opts, sections, metrics)
2565
+ };
2566
+ }
2567
+ function parseLlmResponse5(raw, opts) {
2568
+ let text = raw.trim();
2569
+ if (text.startsWith("```")) {
2570
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2571
+ }
2572
+ let parsed;
2573
+ try {
2574
+ parsed = JSON.parse(text);
2575
+ } catch {
2576
+ return null;
2577
+ }
2578
+ const getString = (key, fallback) => typeof parsed[key] === "string" ? parsed[key] : fallback;
2579
+ const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
2580
+ const sections = rawSections.filter((s) => typeof s.heading === "string").map((s) => {
2581
+ const item = s;
2582
+ return {
2583
+ heading: item.heading,
2584
+ body: typeof item.body === "string" ? item.body : "",
2585
+ type: ["highlight", "metrics", "risk", "shoutout", "upcoming"].includes(item.type) ? item.type : "highlight"
2586
+ };
2587
+ });
2588
+ const rawMetrics = Array.isArray(parsed.metrics) ? parsed.metrics : [];
2589
+ const metrics = rawMetrics.filter((m) => typeof m.label === "string").map((m) => {
2590
+ const item = m;
2591
+ return {
2592
+ label: item.label,
2593
+ value: typeof item.value === "string" ? item.value : "",
2594
+ trend: ["up", "down", "stable"].includes(item.trend) ? item.trend : "stable"
2595
+ };
2596
+ });
2597
+ const subject = getString("subject", "");
2598
+ if (!subject) return null;
2599
+ const slackVersion = getString("slackVersion", "") || buildSlackVersion(opts, sections, metrics);
2600
+ const teamsVersion = getString("teamsVersion", "") || buildTeamsVersion(opts, sections);
2601
+ const htmlVersion = getString("htmlVersion", "") || buildHtmlEmail(opts, sections, metrics);
2602
+ return {
2603
+ subject,
2604
+ preview: getString("preview", "").slice(0, 50),
2605
+ greeting: getString("greeting", `Hello ${opts.teamName ?? "team"},`),
2606
+ headline: getString("headline", ""),
2607
+ sections,
2608
+ metrics,
2609
+ closing: getString("closing", ""),
2610
+ unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
2611
+ slackVersion,
2612
+ teamsVersion,
2613
+ htmlVersion
2614
+ };
2615
+ }
2616
+ async function generateNewsletter(opts) {
2617
+ try {
2618
+ const response = await opts.llm.complete({
2619
+ systemPrompt: buildSystemPrompt11(opts),
2620
+ messages: [{ role: "user", content: buildUserPrompt11(opts) }],
2621
+ maxTokens: 8192
2622
+ });
2623
+ const parsed = parseLlmResponse5(response.content, opts);
2624
+ if (parsed) return parsed;
2625
+ } catch {
2626
+ }
2627
+ return buildFallback5(opts);
2628
+ }
2629
+
2630
+ // src/generators/generateRadio.ts
2631
+ function buildContext4(opts) {
2632
+ const { digestContext: d, amberContext, projectName, audience } = opts;
2633
+ const lines = [
2634
+ `## Architecture Radio Context`,
2635
+ `Project: ${projectName}`,
2636
+ `Audience: ${audience}`,
2637
+ `Period: ${d.period}`,
2638
+ ``,
2639
+ `## Weekly Digest`,
2640
+ `Commits: ${d.commitCount}`,
2641
+ `Files changed: ${d.filesChanged}`,
2642
+ `Coherence grade: ${d.grade ?? "N/A"}`,
2643
+ `Score: ${d.scoreStart ?? "N/A"} \u2192 ${d.scoreEnd ?? "N/A"} (delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"})`,
2644
+ `Health: ${d.healthSummary}`,
2645
+ ``
2646
+ ];
2647
+ if (d.topCommits.length > 0) {
2648
+ lines.push(`### Top Commits`, ...d.topCommits.slice(0, 5).map((c) => `- ${c}`), ``);
2649
+ }
2650
+ if (d.newDrifts.length > 0) {
2651
+ lines.push(`### Capability Drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
2652
+ }
2653
+ if (d.resolvedDrifts.length > 0) {
2654
+ lines.push(`### Resolved Drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
2655
+ }
2656
+ if (d.newRisks.length > 0) {
2657
+ lines.push(`### New Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
2658
+ }
2659
+ if (amberContext) {
2660
+ lines.push(
2661
+ `## AMBER Capability Summary`,
2662
+ `Total capabilities: ${amberContext.capabilities.length}`,
2663
+ `Drifted: ${amberContext.driftedCapabilities}`,
2664
+ `Tagged files: ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`,
2665
+ ``,
2666
+ amberContext.summary,
2667
+ ``
2668
+ );
2669
+ }
2670
+ return lines.join("\n");
2671
+ }
2672
+ function buildSystemPrompt12(audience) {
2673
+ const audienceInstructions = audience === "executive" ? "You are writing for a CTO, VP Engineering, or executive stakeholder. Use ZERO technical jargon. Focus exclusively on business impact: hours saved, risk reduced, team health, delivery confidence. Translate engineering events into business outcomes." : "You are writing for a senior engineering team. Be specific \u2014 include file counts, capability names, cycle details, and concrete numbers. Engineers appreciate precision and honesty.";
2674
+ return [
2675
+ `You are Architecture Radio \u2014 the daily briefing that turns raw engineering signal into clear, compelling communication.`,
2676
+ audienceInstructions,
2677
+ `Keep Slack version punchy and action-oriented (~150 words). Email version has proper structure with greeting, paragraphs, and CTA (~400 words). Twitter version is a shareable insight \u2014 public-safe, no sensitive data, \u2264280 chars.`,
2678
+ `Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
2679
+ ].join(" ");
2680
+ }
2681
+ function buildUserPrompt12(opts) {
2682
+ const context = buildContext4(opts);
2683
+ return [
2684
+ `Generate today's Architecture Radio briefing for: ${opts.projectName}`,
2685
+ `Audience: ${opts.audience}`,
2686
+ ``,
2687
+ context,
2688
+ ``,
2689
+ `## Output Format`,
2690
+ `Return a single JSON object with this exact shape:`,
2691
+ `{`,
2692
+ ` "headline": "<1 powerful sentence capturing the most important thing \u2014 not a title, a statement>",`,
2693
+ ` "slackVersion": "<~150 word Slack mrkdwn message \u2014 punchy, action-oriented, uses *bold*, _italic_, \u2022 bullets>",`,
2694
+ ` "emailVersion": "<~400 word complete HTML email \u2014 full <html><head><body> with inline CSS, dark background, proper greeting, body paragraphs, clear CTA>",`,
2695
+ ` "twitterVersion": "<insight worth sharing publicly \u2014 \u2264280 chars, no sensitive data, no project-specific names unless generic>"`,
2696
+ `}`,
2697
+ ``,
2698
+ `Rules:`,
2699
+ `- headline: not a generic recap \u2014 capture what MATTERS most today`,
2700
+ `- slackVersion: start with the headline, use mrkdwn formatting`,
2701
+ `- emailVersion: full HTML with inline styles, dark-mode friendly (#0f172a background), greeting, 2-3 paragraphs, bold CTA at end`,
2702
+ `- twitterVersion: shareable engineering insight, safe for public, abstract enough for any team`,
2703
+ `- ${opts.audience === "executive" ? "No technical jargon anywhere \u2014 business language only" : "Be technically precise \u2014 engineers hate vague"}`,
2704
+ `- Derive all content from the digest context \u2014 no invented facts`,
2705
+ `- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
2706
+ ].join("\n");
2707
+ }
2708
+ function buildFallback6(opts) {
2709
+ const { digestContext: d, projectName, audience } = opts;
2710
+ const isExec = audience === "executive";
2711
+ const headline = d.newRisks.length > 0 ? isExec ? `${projectName} has ${d.newRisks.length} new architectural risk${d.newRisks.length > 1 ? "s" : ""} requiring attention.` : `${d.newDrifts.length} capability drift${d.newDrifts.length !== 1 ? "s" : ""} detected across ${d.filesChanged} changed files \u2014 action needed.` : isExec ? `${projectName} delivered a healthy week with ${d.commitCount} changes and stable architecture.` : `${d.commitCount} commits, ${d.filesChanged} files \u2014 architecture grade: ${d.grade ?? "N/A"}. ${d.healthSummary}`;
2712
+ const slackVersion = [
2713
+ `*\u{1F4E1} Architecture Radio \u2014 ${d.period}*`,
2714
+ ``,
2715
+ `*${headline}*`,
2716
+ ``,
2717
+ `\u2022 *Health:* ${d.healthSummary}`,
2718
+ d.commitCount > 0 ? `\u2022 *Commits:* ${d.commitCount} across ${d.filesChanged} files` : "",
2719
+ d.newDrifts.length > 0 ? `\u2022 *Drifts:* ${d.newDrifts.slice(0, 3).join(", ")}` : "",
2720
+ d.newRisks.length > 0 ? `\u2022 *Risks:* ${d.newRisks.slice(0, 2).join(", ")}` : "",
2721
+ ``,
2722
+ `_Generated by forge0x2B_`
2723
+ ].filter(Boolean).join("\n");
2724
+ const emailVersion = `<!DOCTYPE html>
2725
+ <html lang="en">
2726
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Architecture Radio \u2014 ${projectName}</title></head>
2727
+ <body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
2728
+ <table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
2729
+ <tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
2730
+ <div style="font-size:11px;color:#f97316;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:8px">\u{1F4E1} Architecture Radio</div>
2731
+ <h1 style="margin:0;font-size:22px;font-weight:700;color:#f1f5f9;line-height:1.3">${headline}</h1>
2732
+ <p style="margin:8px 0 0;color:#64748b;font-size:13px">${d.period}</p>
2733
+ </td></tr>
2734
+ <tr><td style="padding:24px 32px">
2735
+ <p style="margin:0 0 16px;color:#94a3b8;font-size:14px;line-height:1.6">Hello,</p>
2736
+ <p style="margin:0 0 16px;color:#cbd5e1;font-size:14px;line-height:1.6">${d.healthSummary} The team made ${d.commitCount} commit${d.commitCount !== 1 ? "s" : ""} across ${d.filesChanged} file${d.filesChanged !== 1 ? "s" : ""} during ${d.period}.</p>
2737
+ ${d.newDrifts.length > 0 ? `<p style="margin:0 0 16px;color:#cbd5e1;font-size:14px;line-height:1.6"><strong style="color:#f1f5f9">Capability attention needed:</strong> ${d.newDrifts.slice(0, 3).join(", ")}.</p>` : ""}
2738
+ ${d.newRisks.length > 0 ? `<p style="margin:0 0 16px;color:#cbd5e1;font-size:14px;line-height:1.6"><strong style="color:#fca5a5">New risks identified:</strong> ${d.newRisks.slice(0, 3).join(", ")}.</p>` : ""}
2739
+ <p style="margin:24px 0 0;color:#64748b;font-size:12px;border-top:1px solid #1e293b;padding-top:16px">Generated with <a href="https://forge0x2b.dev" style="color:#f97316;text-decoration:none">forge0x2B</a> \xB7 Architecture intelligence for engineering teams</p>
2740
+ </td></tr>
2741
+ </table>
2742
+ </body>
2743
+ </html>`;
2744
+ const twitterVersion = d.scoreDelta !== null ? `Engineering teams that track architectural drift resolve issues ${Math.abs(d.scoreDelta)}x faster. What gets measured gets fixed. \u{1F3D7}\uFE0F` : `The best architecture documentation is one your team actually reads \u2014 and it updates itself. \u{1F4E1}`;
2745
+ return { headline, slackVersion, emailVersion, twitterVersion };
2746
+ }
2747
+ function parseLlmResponse6(raw) {
2748
+ let text = raw.trim();
2749
+ if (text.startsWith("```")) {
2750
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
2751
+ }
2752
+ let parsed;
2753
+ try {
2754
+ parsed = JSON.parse(text);
2755
+ } catch {
2756
+ return null;
2757
+ }
2758
+ const getString = (key) => typeof parsed[key] === "string" ? parsed[key] : "";
2759
+ const headline = getString("headline");
2760
+ if (!headline) return null;
2761
+ return {
2762
+ headline,
2763
+ slackVersion: getString("slackVersion"),
2764
+ emailVersion: getString("emailVersion"),
2765
+ twitterVersion: getString("twitterVersion").slice(0, 280)
2766
+ };
2767
+ }
2768
+ async function generateRadio(options) {
2769
+ try {
2770
+ const response = await options.llm.complete({
2771
+ systemPrompt: buildSystemPrompt12(options.audience),
2772
+ messages: [{ role: "user", content: buildUserPrompt12(options) }],
2773
+ maxTokens: 4096
2774
+ });
2775
+ const parsed = parseLlmResponse6(response.content);
2776
+ if (parsed) return parsed;
2777
+ } catch {
2778
+ }
2779
+ return buildFallback6(options);
2780
+ }
2781
+
2782
+ // src/generators/generateArc42.ts
2783
+ var ARC42_ATTRIBUTION = `arc42 template \xA9 arc42.org, Creative Commons Attribution 4.0 International (CC-BY 4.0).
2784
+ See https://arc42.org for the original template and documentation.`;
2785
+ function buildSection5(opts) {
2786
+ const { amberContext, projectName } = opts;
2787
+ if (!amberContext || amberContext.capabilities.length === 0) {
2788
+ return `## 5. Building Block View
2789
+
2790
+ No AMBER capability data available. Run an AMBER scan to populate this section.
2791
+
2792
+ *This section would show the top-level decomposition of the system into building blocks (capabilities) and their relationships.*
2793
+ `;
2794
+ }
2795
+ const lines = [
2796
+ `## 5. Building Block View`,
2797
+ ``,
2798
+ `*Source: PRISM AMBER capability registry*`,
2799
+ ``,
2800
+ `### Level 1 \u2014 ${projectName} (Whitebox)`,
2801
+ ``,
2802
+ `**Total capabilities:** ${amberContext.capabilities.length}`,
2803
+ `**Tagged files:** ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent.toFixed(0)}%)`,
2804
+ ``,
2805
+ `| Capability | Criticality | Lifecycle | Files |`,
2806
+ `|------------|-------------|-----------|-------|`
2807
+ ];
2808
+ for (const cap of amberContext.capabilities) {
2809
+ lines.push(
2810
+ `| ${cap.name} | ${cap.criticality} | ${cap.lifecycle} | ${cap.files.length} |`
2811
+ );
2812
+ }
2813
+ if (amberContext.driftedCapabilities > 0) {
2814
+ lines.push(
2815
+ ``,
2816
+ `> **Note:** ${amberContext.driftedCapabilities} capabilities have documentation drift (files modified since last doc update).`
2817
+ );
2818
+ }
2819
+ if (amberContext.orphanedFiles.length > 0) {
2820
+ lines.push(
2821
+ ``,
2822
+ `### Orphaned Files (not assigned to any capability)`,
2823
+ ``,
2824
+ ...amberContext.orphanedFiles.slice(0, 10).map((f) => `- \`${f}\``),
2825
+ amberContext.orphanedFiles.length > 10 ? `- *... and ${amberContext.orphanedFiles.length - 10} more*` : ``
2826
+ );
2827
+ }
2828
+ return lines.join("\n");
2829
+ }
2830
+ function buildSection9(opts) {
2831
+ const { digestContext: d } = opts;
2832
+ const lines = [
2833
+ `## 9. Architecture Decisions`,
2834
+ ``,
2835
+ `*Source: PRISM architecture signals*`,
2836
+ ``
2837
+ ];
2838
+ if (d.newRisks.length > 0) {
2839
+ lines.push(
2840
+ `### Recent Architectural Risks Surfaced`,
2841
+ ``,
2842
+ ...d.newRisks.map((r) => `- ${r}`),
2843
+ ``
2844
+ );
2845
+ }
2846
+ if (d.newDrifts.length > 0 || d.resolvedDrifts.length > 0) {
2847
+ lines.push(
2848
+ `### Documentation State`,
2849
+ ``
2850
+ );
2851
+ if (d.newDrifts.length > 0) {
2852
+ lines.push(
2853
+ `**New capability drifts** (${d.newDrifts.length}):`,
2854
+ ...d.newDrifts.map((c) => `- ${c}`),
2855
+ ``
2856
+ );
2857
+ }
2858
+ if (d.resolvedDrifts.length > 0) {
2859
+ lines.push(
2860
+ `**Resolved drifts** (${d.resolvedDrifts.length}):`,
2861
+ ...d.resolvedDrifts.map((c) => `- \u2713 ${c}`),
2862
+ ``
2863
+ );
2864
+ }
2865
+ }
2866
+ lines.push(
2867
+ `### Placeholder`,
2868
+ ``,
2869
+ `*Add ADRs (Architecture Decision Records) here. Use the forge0x2B ADR generator to produce MADR-format decisions from your codebase.*`,
2870
+ ``,
2871
+ `| ID | Title | Status | Date |`,
2872
+ `|----|-------|--------|------|`,
2873
+ `| ADR-001 | *Example decision* | Proposed | ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} |`
2874
+ );
2875
+ return lines.join("\n");
2876
+ }
2877
+ function buildSection10(opts) {
2878
+ const { digestContext: d } = opts;
2879
+ const lines = [
2880
+ `## 10. Quality Requirements`,
2881
+ ``,
2882
+ `*Source: PRISM coherence metrics*`,
2883
+ ``,
2884
+ `### Quality Tree`,
2885
+ ``,
2886
+ `| Quality Goal | Scenario | Priority | Current State |`,
2887
+ `|--------------|----------|----------|---------------|`
2888
+ ];
2889
+ const grade = d.grade ?? "N/A";
2890
+ const gradeColor = grade.startsWith("A") ? "\u2713" : grade.startsWith("B") ? "~" : "\u26A0";
2891
+ lines.push(
2892
+ `| Architecture Coherence | Architecture grade \u2265 B | High | ${gradeColor} ${grade} |`,
2893
+ `| Documentation Coverage | All capabilities documented | Medium | ${d.newDrifts.length === 0 ? "\u2713 No drift" : `\u26A0 ${d.newDrifts.length} drifted`} |`,
2894
+ `| Change Stability | Low churn per capability | Medium | ${d.filesChanged} files changed this period |`
2895
+ );
2896
+ if (d.scoreStart !== null && d.scoreEnd !== null) {
2897
+ const delta = d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A";
2898
+ lines.push(
2899
+ ``,
2900
+ `### Coherence Score Trend`,
2901
+ ``,
2902
+ `| Period | Start | End | Delta |`,
2903
+ `|--------|-------|-----|-------|`,
2904
+ `| ${d.period} | ${d.scoreStart} | ${d.scoreEnd} | ${delta} |`
2905
+ );
2906
+ }
2907
+ return lines.join("\n");
2908
+ }
2909
+ function buildSection11(opts) {
2910
+ const { digestContext: d, amberContext } = opts;
2911
+ const lines = [
2912
+ `## 11. Risks and Technical Debt`,
2913
+ ``,
2914
+ `*Source: PRISM risk signals*`,
2915
+ ``,
2916
+ `### Risk Register`,
2917
+ ``,
2918
+ `| Risk | Probability | Impact | Mitigation |`,
2919
+ `|------|------------|--------|------------|`
2920
+ ];
2921
+ if (d.newRisks.length > 0) {
2922
+ for (const risk of d.newRisks) {
2923
+ lines.push(`| ${risk} | Medium | Medium | Review and address in next sprint |`);
2924
+ }
2925
+ } else {
2926
+ lines.push(`| *No new risks detected in ${d.period}* | \u2014 | \u2014 | \u2014 |`);
2927
+ }
2928
+ if (amberContext && amberContext.driftedCapabilities > 0) {
2929
+ lines.push(`| Documentation drift: ${amberContext.driftedCapabilities} capabilities | High | Low | Run \`prism sync\` |`);
2930
+ }
2931
+ if (d.newDrifts.length > 0) {
2932
+ lines.push(
2933
+ ``,
2934
+ `### Technical Debt \u2014 Documentation Drift`,
2935
+ ``,
2936
+ `The following capabilities have drifted documentation (code changed since last doc update):`,
2937
+ ``,
2938
+ ...d.newDrifts.map((c) => `- \`${c}\``)
2939
+ );
2940
+ }
2941
+ lines.push(
2942
+ ``,
2943
+ `### Health Summary`,
2944
+ ``,
2945
+ `> ${d.healthSummary}`
2946
+ );
2947
+ return lines.join("\n");
2948
+ }
2949
+ function buildSection12(opts) {
2950
+ const { amberContext, projectName } = opts;
2951
+ const lines = [
2952
+ `## 12. Glossary`,
2953
+ ``,
2954
+ `*Source: PRISM AMBER capability registry (Ubiquitous Language)*`,
2955
+ ``,
2956
+ `| Term | Definition |`,
2957
+ `|------|------------|`
2958
+ ];
2959
+ if (amberContext && amberContext.capabilities.length > 0) {
2960
+ for (const cap of amberContext.capabilities) {
2961
+ const desc = cap.description ? cap.description : `${cap.criticality} ${cap.lifecycle} capability in ${projectName}.`;
2962
+ lines.push(`| **${cap.name}** | ${desc} |`);
2963
+ }
2964
+ } else {
2965
+ lines.push(
2966
+ `| *No capabilities registered yet* | Run an AMBER scan to populate the ubiquitous language. |`
2967
+ );
2968
+ }
2969
+ lines.push(
2970
+ ``,
2971
+ `*Add domain-specific terms and acronyms below as the glossary evolves.*`
2972
+ );
2973
+ return lines.join("\n");
2974
+ }
2975
+ function buildSection2() {
2976
+ return `## 2. Constraints
2977
+
2978
+ *Fill in the constraints that apply to your architecture. These are non-negotiable boundaries.*
2979
+
2980
+ ### Technical Constraints
2981
+
2982
+ | Constraint | Background |
2983
+ |------------|------------|
2984
+ | *[e.g. Must run on Kubernetes]* | *[reason]* |
2985
+ | *[e.g. Postgres only \u2014 no other DBs]* | *[reason]* |
2986
+
2987
+ ### Organizational Constraints
2988
+
2989
+ | Constraint | Background |
2990
+ |------------|------------|
2991
+ | *[e.g. Team of N developers]* | *[reason]* |
2992
+ | *[e.g. 2-week sprints]* | *[reason]* |
2993
+
2994
+ ### Conventions
2995
+
2996
+ | Convention | Background |
2997
+ |------------|------------|
2998
+ | *[e.g. TypeScript strict mode]* | *[reason]* |
2999
+ `;
3000
+ }
3001
+ function buildSection6() {
3002
+ return `## 6. Runtime View
3003
+
3004
+ *Describe the important runtime scenarios \u2014 how the system behaves at runtime for key use cases.*
3005
+
3006
+ ### Scenario 1: [Name the scenario]
3007
+
3008
+ \`\`\`
3009
+ [Actor] \u2192 [Component A] \u2192 [Component B] \u2192 [External System]
3010
+ \u2190\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 [Response] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3011
+ \`\`\`
3012
+
3013
+ **Description:** *[Describe what happens step by step.]*
3014
+
3015
+ ### Scenario 2: [Name the scenario]
3016
+
3017
+ *[Add more scenarios as needed.]*
3018
+ `;
3019
+ }
3020
+ function buildSection7() {
3021
+ return `## 7. Deployment View
3022
+
3023
+ *Describe the technical infrastructure \u2014 environments, nodes, and how the system is distributed.*
3024
+
3025
+ ### Infrastructure Level 1
3026
+
3027
+ \`\`\`
3028
+ [Environment: Production]
3029
+ \u2514\u2500\u2500 [Server / Cloud Region]
3030
+ \u251C\u2500\u2500 [Application Tier]
3031
+ \u251C\u2500\u2500 [Database Tier]
3032
+ \u2514\u2500\u2500 [External Services]
3033
+ \`\`\`
3034
+
3035
+ ### Environments
3036
+
3037
+ | Environment | Purpose | URL / Access |
3038
+ |-------------|---------|-------------|
3039
+ | Production | Live system | *[URL]* |
3040
+ | Staging | Pre-production testing | *[URL]* |
3041
+ | Development | Local dev | localhost |
3042
+
3043
+ *Fill in your actual infrastructure topology.*
3044
+ `;
3045
+ }
3046
+ function buildLlmContext(opts) {
3047
+ const { projectName, projectDescription, teamSize, techStack, digestContext: d, amberContext } = opts;
3048
+ const lines = [
3049
+ `Project: ${projectName}`,
3050
+ projectDescription ? `Description: ${projectDescription}` : "",
3051
+ teamSize ? `Team size: ${teamSize}` : "",
3052
+ techStack && techStack.length > 0 ? `Tech stack: ${techStack.join(", ")}` : "",
3053
+ ``,
3054
+ `Architecture health: ${d.healthSummary}`,
3055
+ d.grade ? `Grade: ${d.grade}` : "",
3056
+ `Period: ${d.period}`,
3057
+ `Commits: ${d.commitCount}`,
3058
+ `Files changed: ${d.filesChanged}`,
3059
+ d.topCommits.length > 0 ? `Recent commits: ${d.topCommits.slice(0, 4).join("; ")}` : ""
3060
+ ].filter(Boolean);
3061
+ if (amberContext && amberContext.capabilities.length > 0) {
3062
+ lines.push(
3063
+ ``,
3064
+ `Capabilities: ${amberContext.capabilities.map((c) => c.name).join(", ")}`,
3065
+ `Total capabilities: ${amberContext.capabilities.length}`
3066
+ );
3067
+ }
3068
+ return lines.join("\n");
3069
+ }
3070
+ async function generateLlmSections(opts) {
3071
+ const context = buildLlmContext(opts);
3072
+ const systemPrompt = `You are an expert software architect generating arc42 architecture documentation.
3073
+ Write concise, informative Markdown content for each section.
3074
+ Base your content on the provided PRISM architecture data and project information.
3075
+ Use professional, clear language appropriate for technical documentation.
3076
+ Do not reproduce copyrighted content. All insights are derived from the provided data.`;
3077
+ const userPrompt = `Generate four arc42 documentation sections for this project.
3078
+
3079
+ Project data:
3080
+ ${context}
3081
+
3082
+ Generate the following four sections in Markdown. Each section should start with the exact header shown.
3083
+ Keep each section focused and practical \u2014 100\u2013300 words per section.
3084
+
3085
+ ---
3086
+ ## 1. Introduction and Goals
3087
+
3088
+ Write an overview of:
3089
+ - What the system does (based on the project name, description, and capabilities)
3090
+ - Key quality goals (2\u20133 measurable goals)
3091
+ - Key stakeholders and their expectations
3092
+
3093
+ ---
3094
+ ## 3. Context and Scope
3095
+
3096
+ Write:
3097
+ - A system context description (what the system does, what it connects to)
3098
+ - External systems and actors that interact with the system
3099
+ - What is explicitly out of scope
3100
+
3101
+ ---
3102
+ ## 4. Solution Strategy
3103
+
3104
+ Write:
3105
+ - Core technology decisions and why
3106
+ - Key architectural patterns used (infer from the tech stack and capability structure)
3107
+ - How quality goals are addressed by the architecture
3108
+
3109
+ ---
3110
+ ## 8. Crosscutting Concepts
3111
+
3112
+ Identify crosscutting concerns from the capability names and architecture:
3113
+ - Security / authentication approach
3114
+ - Error handling and logging
3115
+ - Caching strategy (if applicable)
3116
+ - Testing approach
3117
+ - Any other crosscutting patterns visible from the capability structure
3118
+ `;
3119
+ const response = await opts.llm.complete({
3120
+ systemPrompt,
3121
+ messages: [{ role: "user", content: userPrompt }],
3122
+ maxTokens: 4096
3123
+ });
3124
+ const text = response.content;
3125
+ function extractSection(marker, nextMarker) {
3126
+ const start = text.indexOf(marker);
3127
+ if (start === -1) return "";
3128
+ const end = nextMarker ? text.indexOf(nextMarker, start + marker.length) : text.length;
3129
+ return end === -1 ? text.slice(start).trim() : text.slice(start, end).trim();
3130
+ }
3131
+ const s1 = extractSection("## 1. Introduction", "## 3. Context");
3132
+ const s3 = extractSection("## 3. Context", "## 4. Solution");
3133
+ const s4 = extractSection("## 4. Solution", "## 8. Crosscutting");
3134
+ const s8 = extractSection("## 8. Crosscutting", "");
3135
+ return { s1, s3, s4, s8 };
3136
+ }
3137
+ function fallbackSection1(opts) {
3138
+ const { projectName, projectDescription, amberContext } = opts;
3139
+ const capCount = amberContext?.capabilities.length ?? 0;
3140
+ return `## 1. Introduction and Goals
3141
+
3142
+ **${projectName}** ${projectDescription ?? "is a software system documented using arc42."}
3143
+
3144
+ ### Quality Goals
3145
+
3146
+ | Priority | Quality Goal | Scenario |
3147
+ |----------|-------------|----------|
3148
+ | 1 | Correctness | System produces correct results |
3149
+ | 2 | Maintainability | New features can be added without regression |
3150
+ | 3 | Performance | System responds within acceptable time |
3151
+
3152
+ ### Stakeholders
3153
+
3154
+ | Role | Expectations |
3155
+ |------|-------------|
3156
+ | Development Team | Clear architecture structure, ${capCount} documented capabilities |
3157
+ | Product Owner | Feature delivery aligned with roadmap |
3158
+ | Operations | System is deployable and observable |
3159
+
3160
+ *Generated from PRISM data \u2014 enrich with project-specific goals.*
3161
+ `;
3162
+ }
3163
+ function fallbackSection3(opts) {
3164
+ const { projectName } = opts;
3165
+ return `## 3. Context and Scope
3166
+
3167
+ ### System Context
3168
+
3169
+ **${projectName}** interacts with the following external systems:
3170
+
3171
+ | External System | Description | Relationship |
3172
+ |----------------|-------------|-------------|
3173
+ | *[Users / Clients]* | End users of the system | Consumer |
3174
+ | *[External API / Service]* | *[Describe]* | Integration |
3175
+ | *[Database]* | Data persistence | Provider |
3176
+
3177
+ ### In Scope
3178
+
3179
+ *[Describe what the system is responsible for.]*
3180
+
3181
+ ### Out of Scope
3182
+
3183
+ *[Describe what is deliberately excluded.]*
3184
+ `;
3185
+ }
3186
+ function fallbackSection4(opts) {
3187
+ const { techStack } = opts;
3188
+ const stack = techStack && techStack.length > 0 ? techStack.join(", ") : "TypeScript, Next.js";
3189
+ return `## 4. Solution Strategy
3190
+
3191
+ ### Technology Decisions
3192
+
3193
+ **Tech stack:** ${stack}
3194
+
3195
+ | Decision | Rationale |
3196
+ |----------|-----------|
3197
+ | *[Framework choice]* | *[Why this framework]* |
3198
+ | *[Database choice]* | *[Why this DB]* |
3199
+ | *[Architecture pattern]* | *[Why this pattern]* |
3200
+
3201
+ ### Architectural Approach
3202
+
3203
+ *Describe the core architectural approach \u2014 e.g. monolith, microservices, event-driven, CQRS.*
3204
+
3205
+ ### Quality Goal Mapping
3206
+
3207
+ | Quality Goal | Architecture Response |
3208
+ |-------------|----------------------|
3209
+ | Correctness | *[How architecture ensures correctness]* |
3210
+ | Maintainability | *[How architecture supports maintainability]* |
3211
+ `;
3212
+ }
3213
+ function fallbackSection8(opts) {
3214
+ return `## 8. Crosscutting Concepts
3215
+
3216
+ ### Security
3217
+
3218
+ *Describe the authentication and authorization approach.*
3219
+
3220
+ ### Error Handling
3221
+
3222
+ *Describe the error handling and logging strategy.*
3223
+
3224
+ ### Observability
3225
+
3226
+ *Describe how the system is monitored \u2014 logs, metrics, tracing.*
3227
+
3228
+ ### Testing Strategy
3229
+
3230
+ *Describe the testing levels \u2014 unit, integration, end-to-end.*
3231
+
3232
+ ### Configuration
3233
+
3234
+ *Describe how the system is configured across environments.*
3235
+ `;
3236
+ }
3237
+ async function generateArc42(opts) {
3238
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
3239
+ const s5Content = buildSection5(opts);
3240
+ const s9Content = buildSection9(opts);
3241
+ const s10Content = buildSection10(opts);
3242
+ const s11Content = buildSection11(opts);
3243
+ const s12Content = buildSection12(opts);
3244
+ const s2Content = buildSection2();
3245
+ const s6Content = buildSection6();
3246
+ const s7Content = buildSection7();
3247
+ let s1Content;
3248
+ let s3Content;
3249
+ let s4Content;
3250
+ let s8Content;
3251
+ try {
3252
+ const llmSections = await generateLlmSections(opts);
3253
+ s1Content = llmSections.s1 || fallbackSection1(opts);
3254
+ s3Content = llmSections.s3 || fallbackSection3(opts);
3255
+ s4Content = llmSections.s4 || fallbackSection4(opts);
3256
+ s8Content = llmSections.s8 || fallbackSection8(opts);
3257
+ } catch {
3258
+ s1Content = fallbackSection1(opts);
3259
+ s3Content = fallbackSection3(opts);
3260
+ s4Content = fallbackSection4(opts);
3261
+ s8Content = fallbackSection8();
3262
+ }
3263
+ const sections = [
3264
+ { number: 1, title: "Introduction and Goals", content: s1Content, dataSource: "llm" },
3265
+ { number: 2, title: "Constraints", content: s2Content, dataSource: "template" },
3266
+ { number: 3, title: "Context and Scope", content: s3Content, dataSource: "llm" },
3267
+ { number: 4, title: "Solution Strategy", content: s4Content, dataSource: "llm" },
3268
+ { number: 5, title: "Building Block View", content: s5Content, dataSource: "prism" },
3269
+ { number: 6, title: "Runtime View", content: s6Content, dataSource: "template" },
3270
+ { number: 7, title: "Deployment View", content: s7Content, dataSource: "template" },
3271
+ { number: 8, title: "Crosscutting Concepts", content: s8Content, dataSource: "llm" },
3272
+ { number: 9, title: "Architecture Decisions", content: s9Content, dataSource: "prism" },
3273
+ { number: 10, title: "Quality Requirements", content: s10Content, dataSource: "prism" },
3274
+ { number: 11, title: "Risks and Technical Debt", content: s11Content, dataSource: "prism" },
3275
+ { number: 12, title: "Glossary", content: s12Content, dataSource: "prism" }
3276
+ ];
3277
+ const fullMarkdown = [
3278
+ `<!-- This document uses the arc42 template \xA9 arc42.org, licensed under CC-BY 4.0 -->`,
3279
+ `<!-- Generated by PRISM0x2A + forge0x2B \u2014 Architecture intelligence -->`,
3280
+ `<!-- ${generatedAt} -->`,
3281
+ ``,
3282
+ `# Architecture Documentation \u2014 ${opts.projectName}`,
3283
+ ``,
3284
+ `> ${ARC42_ATTRIBUTION}`,
3285
+ ``,
3286
+ ...sections.map((s) => s.content)
3287
+ ].join("\n\n");
3288
+ return {
3289
+ sections,
3290
+ fullMarkdown,
3291
+ attribution: ARC42_ATTRIBUTION,
3292
+ generatedAt
3293
+ };
3294
+ }
3295
+
3296
+ // src/generators/generateKnowledgeCapture.ts
3297
+ function buildContext5(opts) {
3298
+ const { digestContext: d, amberContext, projectName, departingDeveloper, focusCapabilities } = opts;
3299
+ const lines = [
3300
+ `## Knowledge Capture Context`,
3301
+ `Project: ${projectName}`,
3302
+ departingDeveloper ? `Departing Developer: ${departingDeveloper}` : `Departing Developer: (not specified \u2014 write for general knowledge transfer)`,
3303
+ focusCapabilities && focusCapabilities.length > 0 ? `Focus Capabilities: ${focusCapabilities.join(", ")}` : `Focus Capabilities: all`,
3304
+ ``,
3305
+ `## Recent Activity \u2014 ${d.period}`,
3306
+ `Commits: ${d.commitCount}`,
3307
+ `Files changed: ${d.filesChanged}`,
3308
+ `Architecture grade: ${d.grade ?? "N/A"}`,
3309
+ `Health: ${d.healthSummary}`,
3310
+ ``
3311
+ ];
3312
+ if (d.topCommits.length > 0) {
3313
+ lines.push(`### Recent Commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
3314
+ }
3315
+ if (d.newDrifts.length > 0) {
3316
+ lines.push(
3317
+ `### Drifted Capabilities (need documentation attention)`,
3318
+ ...d.newDrifts.map((c) => `- ${c}`),
3319
+ ``
3320
+ );
3321
+ }
3322
+ if (d.newRisks.length > 0) {
3323
+ lines.push(`### Known Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
3324
+ }
3325
+ if (amberContext) {
3326
+ lines.push(`## AMBER Capability Registry`);
3327
+ lines.push(`Total capabilities: ${amberContext.capabilities.length}`);
3328
+ lines.push(`Drifted: ${amberContext.driftedCapabilities}`);
3329
+ lines.push(`Tagged files: ${amberContext.taggedFiles} / ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`);
3330
+ lines.push(``);
3331
+ const toInclude = focusCapabilities && focusCapabilities.length > 0 ? amberContext.capabilities.filter(
3332
+ (c) => focusCapabilities.some((f) => c.name.toLowerCase().includes(f.toLowerCase()) || c.id.toLowerCase().includes(f.toLowerCase()))
3333
+ ) : amberContext.capabilities;
3334
+ if (toInclude.length > 0) {
3335
+ lines.push(`### Capabilities`);
3336
+ for (const cap of toInclude.slice(0, 20)) {
3337
+ lines.push(`#### ${cap.name} (${cap.id})`);
3338
+ lines.push(`- Criticality: ${cap.criticality}`);
3339
+ lines.push(`- Lifecycle: ${cap.lifecycle}`);
3340
+ lines.push(`- Files: ${cap.files.length}`);
3341
+ lines.push(`- Drift: ${cap.driftCount} file${cap.driftCount !== 1 ? "s" : ""} out of date`);
3342
+ if (cap.description) lines.push(`- Description: ${cap.description}`);
3343
+ lines.push(``);
646
3344
  }
647
- return;
648
3345
  }
649
- if (visited.has(node)) return;
650
- visited.add(node);
651
- stack.add(node);
652
- for (const neighbor of graph.get(node) ?? []) {
653
- dfs(neighbor, [...path3, node]);
3346
+ if (amberContext.orphanedFiles.length > 0) {
3347
+ lines.push(
3348
+ `### Orphaned Files (no capability tag)`,
3349
+ ...amberContext.orphanedFiles.slice(0, 10).map((f) => `- ${f}`),
3350
+ ``
3351
+ );
654
3352
  }
655
- stack.delete(node);
656
- }
657
- for (const node of graph.keys()) {
658
- if (!visited.has(node)) dfs(node, []);
659
3353
  }
660
- return cycles;
3354
+ return lines.join("\n");
661
3355
  }
662
- function buildUserPrompt5(blueprint, opts) {
663
- const tone = opts.tone ?? "analytical";
664
- const length = opts.length ?? "medium";
665
- const format = opts.format ?? "markdown";
666
- const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
667
- const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
668
- const importCycles = detectImportCycles(blueprint.edges);
669
- const lines = [
670
- `Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
671
- `Tone: ${tone}. Length: ${length}.`,
3356
+ function buildSystemPrompt13(opts) {
3357
+ const devName = opts.departingDeveloper ?? "a departing developer";
3358
+ return [
3359
+ `You are generating a Knowledge Capture document \u2014 a complete architectural brain dump before ${devName} leaves the team.`,
3360
+ `This document is crucial. It preserves institutional knowledge that would otherwise walk out the door.`,
3361
+ `Write with warmth and humanity \u2014 this is someone's legacy. Be thorough, specific, and genuinely useful.`,
3362
+ `Make it feel like ${devName} wrote it themselves, with care for the next person who will maintain this system.`,
3363
+ `Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
3364
+ ].join(" ");
3365
+ }
3366
+ function buildUserPrompt13(opts) {
3367
+ const context = buildContext5(opts);
3368
+ const devName = opts.departingDeveloper ?? "the departing developer";
3369
+ return [
3370
+ `Generate a complete Knowledge Capture document for: ${opts.projectName}`,
3371
+ `This preserves ${devName}'s architectural knowledge for the team.`,
672
3372
  ``,
673
- `## Architecture Metrics`,
674
- `- Total files: ${blueprint.stats.totalFiles}`,
675
- `- Dependency edges: ${blueprint.stats.runtimeEdges}`,
676
- `- Categories: ${JSON.stringify(blueprint.categories)}`,
3373
+ context,
677
3374
  ``,
678
- `## High-Coupling Hot Spots (high total connections)`,
679
- ...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
3375
+ `## Required Document Structure`,
3376
+ `The document must cover these 6 sections:`,
3377
+ `1. Architecture Overview \u2014 what the system does, how capabilities relate to each other, the mental model`,
3378
+ `2. Critical Capabilities \u2014 deep dive on the most important/drifted/complex capabilities, what makes them tricky`,
3379
+ `3. Known Issues & Workarounds \u2014 things that don't work as expected, temporary hacks, "do not touch" zones`,
3380
+ `4. Where to Start \u2014 recommended reading order for a new developer, which files to read first`,
3381
+ `5. Tribal Knowledge \u2014 things not obvious from code: why decisions were made, what was tried and failed, hidden dependencies`,
3382
+ `6. Onboarding Checklist \u2014 concrete 10-step list for a new team member joining this project`,
680
3383
  ``,
681
- `## High Fan-Out Files (many outgoing dependencies)`,
682
- ...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
3384
+ `## Output Format`,
3385
+ `Return a single JSON object with this exact shape:`,
3386
+ `{`,
3387
+ ` "sections": [`,
3388
+ ` { "title": "<section title>", "content": "<full markdown content for this section, multiple paragraphs>" }`,
3389
+ ` ],`,
3390
+ ` "criticalKnowledge": ["<most important thing 1>", "<most important thing 2>", ...],`,
3391
+ ` "onboardingChecklist": ["<step 1: specific action>", "<step 2: specific action>", ...]`,
3392
+ `}`,
683
3393
  ``,
684
- `## Import Cycles Detected (${importCycles.length})`,
685
- ...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
3394
+ `Rules:`,
3395
+ `- sections: exactly 6, in the order above, with rich content (not just bullets)`,
3396
+ `- criticalKnowledge: 5-8 bullets \u2014 the things a new developer MUST know or they will make mistakes`,
3397
+ `- onboardingChecklist: exactly 10 items \u2014 concrete, actionable steps (not "read the README", but "read src/auth/ starting with session.ts")`,
3398
+ `- Write as if ${devName} is speaking directly to their replacement \u2014 warm, honest, specific`,
3399
+ `- Include file paths and capability names from the context where relevant`,
3400
+ `- Derive all content from the context \u2014 no invented facts`,
3401
+ `- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
3402
+ ].join("\n");
3403
+ }
3404
+ function sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist) {
3405
+ const devName = opts.departingDeveloper ?? "the team";
3406
+ const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
3407
+ const parts = [
3408
+ `# Knowledge Capture: ${opts.projectName}`,
686
3409
  ``,
687
- `Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
3410
+ `> **Prepared by:** ${devName} `,
3411
+ `> **Date:** ${date} `,
3412
+ `> **Purpose:** Preserve architectural knowledge for the next maintainer`,
3413
+ ``,
3414
+ `---`,
3415
+ ``,
3416
+ `## TL;DR \u2014 Critical Knowledge`,
3417
+ ``,
3418
+ ...criticalKnowledge.map((k) => `- ${k}`),
3419
+ ``,
3420
+ `---`,
3421
+ ``
688
3422
  ];
689
- return lines.join("\n");
690
- }
691
- async function generateRefactoringReport(blueprint, opts, provider) {
692
- if (!blueprint) {
693
- return {
694
- text: NO_DATA_MSG4,
695
- metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
696
- };
3423
+ for (const section of sections) {
3424
+ parts.push(`## ${section.title}`, ``, section.content, ``, `---`, ``);
697
3425
  }
698
- const response = await provider.complete({
699
- systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
700
- messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts) }],
701
- maxTokens: 3072
702
- });
703
- return {
704
- text: response.content,
705
- metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
706
- };
707
- }
708
-
709
- // src/generators/askDrivenAsset.ts
710
- var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
711
- var FORMAT_GUIDES = {
712
- markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
713
- blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
714
- social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
715
- email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
716
- slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
717
- slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
718
- };
719
- function buildSystemPrompt6(format, tone) {
720
- const toneMap = {
721
- professional: "You are a professional technical writer and developer advocate.",
722
- casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
723
- technical: "You are a senior software engineer writing precise, implementation-focused content.",
724
- executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
725
- };
726
- const guide = FORMAT_GUIDES[format];
727
- return `${toneMap[tone] ?? toneMap.professional} Generate a ${guide.name} based on the user's question and the provided codebase architecture context. ${guide.structure} Write only the requested content \u2014 no preamble, no meta-commentary.`;
728
- }
729
- function buildUserPrompt6(blueprint, question, opts) {
730
- const format = opts.format ?? "markdown";
731
- const tone = opts.tone ?? "professional";
732
- const length = opts.length ?? "medium";
733
- const guide = FORMAT_GUIDES[format];
734
- const lengthGuide = {
735
- short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
736
- medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
737
- long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
738
- }[length];
739
- const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
740
- const lines = [
741
- `## User's Question`,
742
- question,
3426
+ parts.push(
3427
+ `## Onboarding Checklist`,
743
3428
  ``,
744
- `## Output Requirements`,
745
- `- Format: ${guide.name}`,
746
- `- Tone: ${tone}`,
747
- `- Length: ${length} (${lengthGuide})`,
3429
+ `For the next person joining this project:`,
748
3430
  ``,
749
- `## Codebase Architecture Context`,
750
- `Target: ${blueprint.targetPath}`,
751
- `Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
752
- `Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
3431
+ ...onboardingChecklist.map((step, i) => `- [ ] **Step ${i + 1}:** ${step}`),
753
3432
  ``,
754
- `Key files (by usage):`,
755
- ...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
3433
+ `---`,
3434
+ ``,
3435
+ `*Generated with [forge0x2B](https://forge0x2b.dev) \xB7 Architecture intelligence for engineering teams*`
3436
+ );
3437
+ return parts.join("\n");
3438
+ }
3439
+ function buildFallback7(opts) {
3440
+ const { digestContext: d, amberContext, projectName, departingDeveloper } = opts;
3441
+ const critCaps = amberContext?.capabilities.filter((c) => c.criticality === "critical" || c.driftCount > 0).slice(0, 5).map((c) => c.name) ?? [];
3442
+ const sections = [
3443
+ {
3444
+ title: "Architecture Overview",
3445
+ content: `${projectName} is composed of ${amberContext?.capabilities.length ?? "several"} tracked capabilities. ${d.healthSummary}
3446
+
3447
+ The system currently has ${d.filesChanged} recently changed files and a coherence grade of ${d.grade ?? "unknown"}.`
3448
+ },
3449
+ {
3450
+ title: "Critical Capabilities",
3451
+ content: critCaps.length > 0 ? `The following capabilities require the most attention:
3452
+
3453
+ ${critCaps.map((c) => `**${c}** \u2014 review all files tagged with this capability`).join("\n\n")}` : `Review the AMBER capability registry for the current capability map and any drift indicators.`
3454
+ },
3455
+ {
3456
+ title: "Known Issues & Workarounds",
3457
+ content: d.newRisks.length > 0 ? `The following risks have been identified:
3458
+
3459
+ ${d.newRisks.map((r) => `- **${r}**: Review related files before making changes`).join("\n")}` : `No critical known issues at time of writing. Check the PRISM risk registry for the latest state.`
3460
+ },
3461
+ {
3462
+ title: "Where to Start",
3463
+ content: `Start by understanding the capability structure. Read \`.amber/capabilities.md\` for the full capability registry, then \`.amber/state.json\` for file-to-capability mappings.
3464
+
3465
+ Focus on capabilities with high drift counts first \u2014 those are where documentation has fallen behind the code.`
3466
+ },
3467
+ {
3468
+ title: "Tribal Knowledge",
3469
+ content: `Key things not obvious from reading the code:
3470
+
3471
+ - The AMBER tags in source files are the source of truth for capability ownership
3472
+ - Drift count indicates documentation debt \u2014 files changed without updating @amber-doc tags
3473
+ - Architecture score changes over time \u2014 check \`.green/\` for trend data`
3474
+ },
3475
+ {
3476
+ title: "Architecture Decisions",
3477
+ content: `${departingDeveloper ? `${departingDeveloper} made` : "Key"} architectural decisions are not yet fully documented. Recommended: schedule a knowledge transfer session and ask specifically about: capability boundaries, cross-capability dependencies, and any "do not touch" areas.`
3478
+ }
756
3479
  ];
757
- if (blueprint.edges.length > 0) {
758
- const edgeSample = blueprint.edges.slice(0, 15);
759
- lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
760
- }
761
- lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
762
- return lines.join("\n");
3480
+ const criticalKnowledge = [
3481
+ `The AMBER capability registry (.amber/) is the source of truth for how the codebase is organized`,
3482
+ `Drift count > 0 means documentation hasn't kept pace with code changes \u2014 fix before adding features`,
3483
+ amberContext ? `${amberContext.driftedCapabilities} of ${amberContext.capabilities.length} capabilities currently have drift` : `Check drift count before starting work`,
3484
+ d.newRisks.length > 0 ? `${d.newRisks.length} active risk${d.newRisks.length > 1 ? "s" : ""} identified: ${d.newRisks.slice(0, 2).join(", ")}` : `Monitor architecture score for regression`,
3485
+ `Architecture grade: ${d.grade ?? "unknown"} \u2014 understand what's driving this before making broad changes`
3486
+ ].filter(Boolean);
3487
+ const onboardingChecklist = [
3488
+ `Read this entire document before writing any code`,
3489
+ `Install and run PRISM to get a live architecture view`,
3490
+ `Review .amber/capabilities.md \u2014 understand every capability and its criticality`,
3491
+ `Check .amber/state.json to see which files belong to which capabilities`,
3492
+ `Run the test suite and confirm it passes before making changes`,
3493
+ `Read the top ${Math.min(d.topCommits.length || 5, 5)} recent commits to understand current momentum`,
3494
+ `Review capabilities with drift count > 0 \u2014 read both the code and the docs`,
3495
+ `Set up forge0x2B to get daily Architecture Radio briefings`,
3496
+ `Ask the team about any "do not touch" areas that aren't documented`,
3497
+ `Make your first PR small and well-scoped to verify your understanding`
3498
+ ];
3499
+ const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
3500
+ return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
763
3501
  }
764
- async function generateAskDrivenAsset(blueprint, question, opts, provider) {
765
- if (!blueprint) {
766
- return {
767
- text: NO_DATA_MSG5,
768
- metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
769
- };
3502
+ function parseLlmResponse7(raw, opts) {
3503
+ let text = raw.trim();
3504
+ if (text.startsWith("```")) {
3505
+ text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
770
3506
  }
771
- if (!question || !question.trim()) {
3507
+ let parsed;
3508
+ try {
3509
+ parsed = JSON.parse(text);
3510
+ } catch {
3511
+ return null;
3512
+ }
3513
+ const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
3514
+ const sections = rawSections.filter((s) => typeof s.title === "string").map((s) => {
3515
+ const item = s;
772
3516
  return {
773
- text: "No question provided. Please ask something about your codebase.",
774
- metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
3517
+ title: item.title,
3518
+ content: typeof item.content === "string" ? item.content : ""
775
3519
  };
776
- }
777
- const format = opts.format ?? "markdown";
778
- const tone = opts.tone ?? "professional";
779
- const maxTokens = FORMAT_GUIDES[format].maxTokens;
780
- const response = await provider.complete({
781
- systemPrompt: buildSystemPrompt6(format, tone),
782
- messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
783
- maxTokens
784
3520
  });
785
- return {
786
- text: response.content,
787
- metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
788
- };
3521
+ if (sections.length === 0) return null;
3522
+ const rawCritical = Array.isArray(parsed.criticalKnowledge) ? parsed.criticalKnowledge : [];
3523
+ const criticalKnowledge = rawCritical.filter((k) => typeof k === "string").map((k) => k);
3524
+ const rawChecklist = Array.isArray(parsed.onboardingChecklist) ? parsed.onboardingChecklist : [];
3525
+ const onboardingChecklist = rawChecklist.filter((k) => typeof k === "string").map((k) => k);
3526
+ const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
3527
+ return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
3528
+ }
3529
+ async function generateKnowledgeCapture(options) {
3530
+ try {
3531
+ const response = await options.llm.complete({
3532
+ systemPrompt: buildSystemPrompt13(options),
3533
+ messages: [{ role: "user", content: buildUserPrompt13(options) }],
3534
+ maxTokens: 8192
3535
+ });
3536
+ const parsed = parseLlmResponse7(response.content, options);
3537
+ if (parsed) return parsed;
3538
+ } catch {
3539
+ }
3540
+ return buildFallback7(options);
789
3541
  }
790
3542
 
791
3543
  // src/forge/types.ts
@@ -1712,63 +4464,63 @@ function initialFormValues(fields, source) {
1712
4464
  function isEmptyString(v) {
1713
4465
  return typeof v === "string" && v.trim().length === 0;
1714
4466
  }
1715
- function validateField(field, value, path3) {
4467
+ function validateField(field, value, path5) {
1716
4468
  const errors = {};
1717
4469
  switch (field.kind) {
1718
4470
  case "string": {
1719
4471
  if (field.required && (value === void 0 || value === null || isEmptyString(value))) {
1720
- errors[path3] = `${field.label} ist erforderlich`;
4472
+ errors[path5] = `${field.label} ist erforderlich`;
1721
4473
  break;
1722
4474
  }
1723
4475
  if (typeof value === "string") {
1724
4476
  if (field.minLength !== void 0 && value.length < field.minLength) {
1725
- errors[path3] = `${field.label} mindestens ${field.minLength} Zeichen`;
4477
+ errors[path5] = `${field.label} mindestens ${field.minLength} Zeichen`;
1726
4478
  } else if (field.maxLength !== void 0 && value.length > field.maxLength) {
1727
- errors[path3] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
4479
+ errors[path5] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
1728
4480
  } else if (field.enum && value.length > 0 && !field.enum.includes(value)) {
1729
- errors[path3] = `${field.label}: Wert nicht in der Auswahl`;
4481
+ errors[path5] = `${field.label}: Wert nicht in der Auswahl`;
1730
4482
  }
1731
4483
  }
1732
4484
  break;
1733
4485
  }
1734
4486
  case "number": {
1735
4487
  if (value === "" || value === void 0 || value === null) {
1736
- if (field.required) errors[path3] = `${field.label} ist erforderlich`;
4488
+ if (field.required) errors[path5] = `${field.label} ist erforderlich`;
1737
4489
  break;
1738
4490
  }
1739
4491
  const n = typeof value === "number" ? value : Number(value);
1740
4492
  if (!Number.isFinite(n)) {
1741
- errors[path3] = `${field.label} muss eine Zahl sein`;
4493
+ errors[path5] = `${field.label} muss eine Zahl sein`;
1742
4494
  } else if (field.integer && !Number.isInteger(n)) {
1743
- errors[path3] = `${field.label} muss eine ganze Zahl sein`;
4495
+ errors[path5] = `${field.label} muss eine ganze Zahl sein`;
1744
4496
  } else if (field.minimum !== void 0 && n < field.minimum) {
1745
- errors[path3] = `${field.label} >= ${field.minimum}`;
4497
+ errors[path5] = `${field.label} >= ${field.minimum}`;
1746
4498
  } else if (field.maximum !== void 0 && n > field.maximum) {
1747
- errors[path3] = `${field.label} <= ${field.maximum}`;
4499
+ errors[path5] = `${field.label} <= ${field.maximum}`;
1748
4500
  }
1749
4501
  break;
1750
4502
  }
1751
4503
  case "boolean":
1752
4504
  if (field.required && value !== true) {
1753
- errors[path3] = `${field.label} muss aktiviert sein`;
4505
+ errors[path5] = `${field.label} muss aktiviert sein`;
1754
4506
  }
1755
4507
  break;
1756
4508
  case "array": {
1757
4509
  const arr = Array.isArray(value) ? value : [];
1758
4510
  if (field.required && arr.length === 0) {
1759
- errors[path3] = `${field.label}: mindestens ein Eintrag erforderlich`;
4511
+ errors[path5] = `${field.label}: mindestens ein Eintrag erforderlich`;
1760
4512
  }
1761
4513
  if (field.minItems !== void 0 && arr.length < field.minItems) {
1762
- errors[path3] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
4514
+ errors[path5] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
1763
4515
  } else if (field.maxItems !== void 0 && arr.length > field.maxItems) {
1764
- errors[path3] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
4516
+ errors[path5] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
1765
4517
  }
1766
4518
  if (field.itemField) {
1767
4519
  arr.forEach((item, i) => {
1768
4520
  const sub = validateField(
1769
4521
  field.itemField,
1770
4522
  item,
1771
- `${path3}[${i}]`
4523
+ `${path5}[${i}]`
1772
4524
  );
1773
4525
  Object.assign(errors, sub);
1774
4526
  });
@@ -1778,7 +4530,7 @@ function validateField(field, value, path3) {
1778
4530
  case "object": {
1779
4531
  const obj = isPlainObject5(value) ? value : {};
1780
4532
  for (const sub of field.fields) {
1781
- const subErrors = validateField(sub, obj[sub.name], `${path3}.${sub.name}`);
4533
+ const subErrors = validateField(sub, obj[sub.name], `${path5}.${sub.name}`);
1782
4534
  Object.assign(errors, subErrors);
1783
4535
  }
1784
4536
  break;
@@ -2273,6 +5025,80 @@ var WIDGET_TEMPLATE_SOCIAL_PROOF = {
2273
5025
  { id: "show_divider", label: "Show divider", type: "select", options: ["yes", "no"], default: "no" }
2274
5026
  ]
2275
5027
  };
5028
+ var WIDGET_TEMPLATE_ANIMATED_STAT = {
5029
+ id: "animated-stat",
5030
+ name: "Animated Stat",
5031
+ description: "Full-card animated number counter. Perfect for LinkedIn/Instagram. Export as video or GIF.",
5032
+ free_tier: false,
5033
+ animated: true,
5034
+ animDurationMs: 2200,
5035
+ videoDurationMs: 3800,
5036
+ defaultWidth: 600,
5037
+ defaultHeight: 400,
5038
+ exportFormats: ["standalone", "html", "markdown"],
5039
+ slots: [
5040
+ { id: "value", label: "Value (number)", type: "number", required: true, placeholder: "138", default: "138" },
5041
+ { id: "label", label: "Label", type: "text", required: true, placeholder: "Files analysed", default: "Files analysed" },
5042
+ { id: "unit", label: "Unit (optional)", type: "text", placeholder: "k LOC", default: "" },
5043
+ { id: "sublabel", label: "Sublabel / context", type: "text", placeholder: "across 3 modules", default: "" },
5044
+ { id: "delta", label: "Delta text", type: "text", placeholder: "+22% this sprint", default: "" },
5045
+ { id: "delta_direction", label: "Delta direction", type: "select", options: ["up", "down", "neutral"], default: "up" }
5046
+ ]
5047
+ };
5048
+ var WIDGET_TEMPLATE_RELEASE_CARD = {
5049
+ id: "release-card",
5050
+ name: "Release Card",
5051
+ description: "Animated release announcement \u2014 version, headline, and staggered change list. Great for LinkedIn posts.",
5052
+ free_tier: false,
5053
+ animated: true,
5054
+ animDurationMs: 1200,
5055
+ videoDurationMs: 3e3,
5056
+ defaultWidth: 600,
5057
+ defaultHeight: 400,
5058
+ exportFormats: ["standalone", "html", "markdown"],
5059
+ slots: [
5060
+ { id: "version", label: "Version", type: "text", required: true, placeholder: "v2.4.0", default: "v2.4.0" },
5061
+ { id: "headline", label: "Headline", type: "text", required: true, placeholder: "Faster. Smarter. Leaner.", default: "Faster. Smarter. Leaner." },
5062
+ { id: "changes", label: "Changes (one per line)", type: "multiline", required: true, placeholder: "40% faster cold starts\nNew Capability Registry\nFixed circular import detection", default: "40% faster cold starts\nNew Capability Registry\nFixed circular import detection" },
5063
+ { id: "tag", label: "Release tag", type: "select", options: ["feat", "fix", "perf", "break", ""], default: "feat" }
5064
+ ]
5065
+ };
5066
+ var WIDGET_TEMPLATE_CHART_BARS = {
5067
+ id: "chart-bars",
5068
+ name: "Chart \u2014 Bars",
5069
+ description: "Animated Chart.js bar chart from your codebase metrics. Perfect for visualising churn, coverage, or LOC.",
5070
+ free_tier: false,
5071
+ animated: true,
5072
+ animDurationMs: 1600,
5073
+ videoDurationMs: 3200,
5074
+ defaultWidth: 640,
5075
+ defaultHeight: 420,
5076
+ exportFormats: ["standalone", "html", "markdown"],
5077
+ slots: [
5078
+ { id: "title", label: "Chart title", type: "text", placeholder: "Files by module", default: "Files by module" },
5079
+ { id: "labels", label: "Labels (one per line)", type: "multiline", required: true, placeholder: "api\nlib\ncomponents\napp", default: "api\nlib\ncomponents\napp" },
5080
+ { id: "values", label: "Values (one per line)", type: "multiline", required: true, placeholder: "24\n18\n41\n12", default: "24\n18\n41\n12" },
5081
+ { id: "orientation", label: "Orientation", type: "select", options: ["vertical", "horizontal"], default: "vertical" },
5082
+ { id: "accent_color", label: "Bar colour (CSS)", type: "color", placeholder: "#f97316", default: "" }
5083
+ ]
5084
+ };
5085
+ var WIDGET_TEMPLATE_ARCHITECTURE_BADGE = {
5086
+ id: "architecture-badge",
5087
+ name: "Architecture Badge",
5088
+ description: "Animated tech-stack showcase with Lucide icons. Items pop in with spring animation \u2014 great for profile posts.",
5089
+ free_tier: false,
5090
+ animated: true,
5091
+ animDurationMs: 900,
5092
+ videoDurationMs: 2500,
5093
+ defaultWidth: 580,
5094
+ defaultHeight: 360,
5095
+ exportFormats: ["standalone", "html", "markdown"],
5096
+ slots: [
5097
+ { id: "title", label: "Title", type: "text", placeholder: "Built with", default: "Built with" },
5098
+ { id: "items", label: "Stack items (one per line)", type: "multiline", required: true, placeholder: "TypeScript\nNext.js\nPostgres\nTailwind", default: "TypeScript\nNext.js\nPostgres\nTailwind" },
5099
+ { id: "subtitle", label: "Subtitle / tagline", type: "text", placeholder: "100% local-first \xB7 zero telemetry", default: "" }
5100
+ ]
5101
+ };
2276
5102
  var BUNDLED_WIDGET_TEMPLATES = [
2277
5103
  WIDGET_TEMPLATE_STAT_CARD,
2278
5104
  WIDGET_TEMPLATE_FEATURE_GRID,
@@ -2281,7 +5107,12 @@ var BUNDLED_WIDGET_TEMPLATES = [
2281
5107
  WIDGET_TEMPLATE_METRIC_BADGE,
2282
5108
  WIDGET_TEMPLATE_PRICING_TIER,
2283
5109
  WIDGET_TEMPLATE_CHANGELOG_ROW,
2284
- WIDGET_TEMPLATE_SOCIAL_PROOF
5110
+ WIDGET_TEMPLATE_SOCIAL_PROOF,
5111
+ // Animated — standalone + video export
5112
+ WIDGET_TEMPLATE_ANIMATED_STAT,
5113
+ WIDGET_TEMPLATE_RELEASE_CARD,
5114
+ WIDGET_TEMPLATE_CHART_BARS,
5115
+ WIDGET_TEMPLATE_ARCHITECTURE_BADGE
2285
5116
  ];
2286
5117
  var FREE_TIER_WIDGET_IDS = BUNDLED_WIDGET_TEMPLATES.filter((t) => t.free_tier).map((t) => t.id);
2287
5118
  function getWidgetTemplate(id) {
@@ -2440,6 +5271,184 @@ ${divider ? `<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);m
2440
5271
  ${logos.map((l) => ` <span style="font-weight:600;font-size:.875rem;color:var(--pw-brand-muted);letter-spacing:-.01em">${escHtml(l)}</span>`).join("\n")}
2441
5272
  </div>`;
2442
5273
  }
5274
+ function renderAnimatedStatHtml(slots, t) {
5275
+ const value = getSlotValue(slots, t, "value");
5276
+ const label = getSlotValue(slots, t, "label");
5277
+ const unit = getSlotValue(slots, t, "unit");
5278
+ const sublabel = getSlotValue(slots, t, "sublabel");
5279
+ const dir = getSlotValue(slots, t, "delta_direction");
5280
+ const delta = getSlotValue(slots, t, "delta");
5281
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
5282
+ const deltaColor = dir === "up" ? "#22c55e" : dir === "down" ? "#ef4444" : "var(--pw-brand-muted)";
5283
+ return `<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:240px;text-align:center;gap:var(--pw-spacing-sm)">
5284
+ <div id="pw-anim-value" style="font-size:5rem;font-weight:900;line-height:1;font-family:var(--pw-font-heading);color:var(--pw-brand-primary);letter-spacing:-0.04em">0</div>
5285
+ ${unit ? `<div style="font-size:1.25rem;font-weight:600;color:var(--pw-brand-muted);margin-top:-var(--pw-spacing-sm)">${escHtml(unit)}</div>` : ""}
5286
+ <div style="font-size:1.1rem;font-weight:600;color:var(--pw-brand-text);letter-spacing:0.01em">${escHtml(label)}</div>
5287
+ ${sublabel ? `<div style="font-size:.8rem;color:var(--pw-brand-muted)">${escHtml(sublabel)}</div>` : ""}
5288
+ ${delta ? `<div style="font-size:.875rem;color:${deltaColor};font-weight:600">${arrow}${arrow ? " " : ""}${escHtml(delta)}</div>` : ""}
5289
+ </div>
5290
+ <script>
5291
+ (function(){
5292
+ var target = ${Number(value.replace(/[^0-9.-]/g, "")) || 0};
5293
+ var isFloat = target % 1 !== 0;
5294
+ var obj = { val: 0 };
5295
+ anime({
5296
+ targets: obj,
5297
+ val: target,
5298
+ duration: ${2e3},
5299
+ easing: "easeOutExpo",
5300
+ update: function() {
5301
+ document.getElementById("pw-anim-value").textContent =
5302
+ isFloat ? obj.val.toFixed(1) : Math.round(obj.val).toLocaleString();
5303
+ }
5304
+ });
5305
+ })();
5306
+ </script>`;
5307
+ }
5308
+ function renderReleaseCardHtml(slots, t) {
5309
+ const version = getSlotValue(slots, t, "version");
5310
+ const headline = getSlotValue(slots, t, "headline");
5311
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
5312
+ const tag = getSlotValue(slots, t, "tag");
5313
+ const tagColors = { feat: "#22c55e", fix: "#f59e0b", perf: "#60a5fa", break: "#ef4444" };
5314
+ const tc = tagColors[tag] ?? "var(--pw-brand-accent)";
5315
+ return `<div class="pw-release-wrap" style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
5316
+ <div style="display:flex;align-items:center;gap:var(--pw-spacing-sm);opacity:0" class="pw-ri">
5317
+ <span style="font-family:var(--pw-font-mono);font-size:1.1rem;font-weight:700;color:var(--pw-brand-primary)">${escHtml(version)}</span>
5318
+ ${tag ? `<span style="font-size:.7rem;padding:2px 8px;border-radius:var(--pw-radius-full);background:${tc}22;color:${tc};font-weight:700;text-transform:uppercase;letter-spacing:.06em">${escHtml(tag)}</span>` : ""}
5319
+ </div>
5320
+ <h2 style="margin:0;font-size:1.5rem;font-weight:800;line-height:1.25;font-family:var(--pw-font-heading);opacity:0" class="pw-ri">${escHtml(headline)}</h2>
5321
+ <ul style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--pw-spacing-xs)">
5322
+ ${changes.map((c) => `<li class="pw-change-item" style="display:flex;align-items:flex-start;gap:var(--pw-spacing-xs);font-size:.9rem;opacity:0"><span style="color:${tc};flex-shrink:0;margin-top:2px">\u2192</span><span>${escHtml(c)}</span></li>`).join("\n ")}
5323
+ </ul>
5324
+ </div>
5325
+ <script>
5326
+ (function(){
5327
+ anime({ targets: ".pw-ri", translateY: [-16, 0], opacity: [0, 1], delay: anime.stagger(120), duration: 500, easing: "easeOutCubic" });
5328
+ anime({ targets: ".pw-change-item", translateX: [-20, 0], opacity: [0, 1], delay: anime.stagger(80, { start: 350 }), duration: 450, easing: "easeOutCubic" });
5329
+ })();
5330
+ </script>`;
5331
+ }
5332
+ function renderChartBarsHtml(slots, t) {
5333
+ const title = getSlotValue(slots, t, "title");
5334
+ const labelsRaw = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
5335
+ const valuesRaw = getSlotValue(slots, t, "values").split("\n").filter(Boolean).map(Number);
5336
+ const orientation = getSlotValue(slots, t, "orientation") || "vertical";
5337
+ getSlotValue(slots, t, "accent_color") || "var(--pw-brand-primary)";
5338
+ const labelsJson = JSON.stringify(labelsRaw);
5339
+ const valuesJson = JSON.stringify(valuesRaw);
5340
+ const chartType = orientation === "horizontal" ? "bar" : "bar";
5341
+ const indexAxis = orientation === "horizontal" ? `indexAxis: "y",` : "";
5342
+ return `${title ? `<h3 style="margin:0 0 var(--pw-spacing-md);font-size:1rem;font-weight:700;font-family:var(--pw-font-heading);color:var(--pw-brand-text)">${escHtml(title)}</h3>` : ""}
5343
+ <div style="position:relative;width:100%;height:220px">
5344
+ <canvas id="pw-chart" style="width:100%;height:100%"></canvas>
5345
+ </div>
5346
+ <script>
5347
+ (function(){
5348
+ var ctx = document.getElementById("pw-chart").getContext("2d");
5349
+ var accent = getComputedStyle(document.querySelector(".pw-widget")).getPropertyValue("--pw-brand-primary").trim() || "#f97316";
5350
+ new Chart(ctx, {
5351
+ type: "${chartType}",
5352
+ data: {
5353
+ labels: ${labelsJson},
5354
+ datasets: [{
5355
+ data: ${valuesJson},
5356
+ backgroundColor: accent + "cc",
5357
+ borderColor: accent,
5358
+ borderWidth: 2,
5359
+ borderRadius: 6,
5360
+ }]
5361
+ },
5362
+ options: {
5363
+ ${indexAxis}
5364
+ responsive: true,
5365
+ maintainAspectRatio: false,
5366
+ animation: { duration: 1500, easing: "easeOutQuart" },
5367
+ plugins: { legend: { display: false } },
5368
+ scales: {
5369
+ x: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } },
5370
+ y: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } }
5371
+ }
5372
+ }
5373
+ });
5374
+ })();
5375
+ </script>`;
5376
+ }
5377
+ var LUCIDE_ICON_MAP = {
5378
+ typescript: "code-2",
5379
+ javascript: "code-2",
5380
+ "js": "code-2",
5381
+ "ts": "code-2",
5382
+ react: "atom",
5383
+ next: "triangle",
5384
+ nextjs: "triangle",
5385
+ "next.js": "triangle",
5386
+ node: "server",
5387
+ nodejs: "server",
5388
+ "node.js": "server",
5389
+ postgres: "database",
5390
+ postgresql: "database",
5391
+ mysql: "database",
5392
+ sqlite: "database",
5393
+ supabase: "database",
5394
+ prisma: "layers",
5395
+ tailwind: "palette",
5396
+ css: "palette",
5397
+ sass: "palette",
5398
+ docker: "box",
5399
+ kubernetes: "cloud",
5400
+ aws: "cloud",
5401
+ vercel: "triangle",
5402
+ github: "git-branch",
5403
+ git: "git-branch",
5404
+ graphql: "share-2",
5405
+ rest: "globe",
5406
+ api: "globe",
5407
+ redis: "zap",
5408
+ kafka: "radio",
5409
+ python: "terminal",
5410
+ rust: "cpu",
5411
+ go: "activity",
5412
+ java: "coffee",
5413
+ vue: "layers",
5414
+ svelte: "layers",
5415
+ angular: "layers",
5416
+ figma: "pen-tool",
5417
+ linear: "layers",
5418
+ default: "layers"
5419
+ };
5420
+ function renderArchitectureBadgeHtml(slots, t) {
5421
+ const title = getSlotValue(slots, t, "title");
5422
+ const subtitle = getSlotValue(slots, t, "subtitle");
5423
+ const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
5424
+ const palette = ["#60a5fa", "#f97316", "#34d399", "#a78bfa", "#fb923c", "#38bdf8", "#facc15", "#f472b6"];
5425
+ const chips = items.map((item, i) => {
5426
+ const key = item.trim().toLowerCase().replace(/[^a-z0-9.]/g, "");
5427
+ const icon = LUCIDE_ICON_MAP[key] ?? LUCIDE_ICON_MAP.default;
5428
+ const color = palette[i % palette.length];
5429
+ return `<div class="pw-arch-item" style="display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:var(--pw-radius-full);border:1px solid ${color}33;background:${color}11;opacity:0">
5430
+ <i data-lucide="${icon}" style="width:14px;height:14px;color:${color};flex-shrink:0"></i>
5431
+ <span style="font-size:.8rem;font-weight:600;color:${color};letter-spacing:.02em">${escHtml(item.trim())}</span>
5432
+ </div>`;
5433
+ }).join("\n ");
5434
+ return `<div style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
5435
+ ${title ? `<h3 style="margin:0;font-size:1.1rem;font-weight:700;font-family:var(--pw-font-heading);color:var(--pw-brand-text);opacity:0" id="pw-arch-title">${escHtml(title)}</h3>` : ""}
5436
+ <div style="display:flex;flex-wrap:wrap;gap:var(--pw-spacing-xs)">
5437
+ ${chips}
5438
+ </div>
5439
+ ${subtitle ? `<p style="margin:0;font-size:.75rem;color:var(--pw-brand-muted);opacity:0" class="pw-arch-sub">${escHtml(subtitle)}</p>` : ""}
5440
+ </div>
5441
+ <script>
5442
+ (function(){
5443
+ lucide.createIcons();
5444
+ var title = document.getElementById("pw-arch-title");
5445
+ if(title) anime({ targets: title, opacity: [0,1], translateY: [-10,0], duration: 400, easing: "easeOutCubic" });
5446
+ anime({ targets: ".pw-arch-item", opacity: [0,1], scale: [0.85,1], delay: anime.stagger(70, { start: 200 }), duration: 400, easing: "easeOutBack" });
5447
+ var sub = document.querySelector(".pw-arch-sub");
5448
+ if(sub) anime({ targets: sub, opacity: [0,1], delay: 600, duration: 400, easing: "easeOutCubic" });
5449
+ })();
5450
+ </script>`;
5451
+ }
2443
5452
  var HTML_BODY_FN = {
2444
5453
  "stat-card": renderStatCardHtml,
2445
5454
  "feature-grid": renderFeatureGridHtml,
@@ -2448,7 +5457,11 @@ var HTML_BODY_FN = {
2448
5457
  "metric-badge": renderMetricBadgeHtml,
2449
5458
  "pricing-tier": renderPricingTierHtml,
2450
5459
  "changelog-row": renderChangelogRowHtml,
2451
- "social-proof": renderSocialProofHtml
5460
+ "social-proof": renderSocialProofHtml,
5461
+ "animated-stat": renderAnimatedStatHtml,
5462
+ "release-card": renderReleaseCardHtml,
5463
+ "chart-bars": renderChartBarsHtml,
5464
+ "architecture-badge": renderArchitectureBadgeHtml
2452
5465
  };
2453
5466
  function renderStatCardMd(slots, t) {
2454
5467
  const value = getSlotValue(slots, t, "value");
@@ -2519,6 +5532,43 @@ function renderSocialProofMd(slots, t) {
2519
5532
  const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
2520
5533
  return `${caption ? caption + "\n\n" : ""}${logos.join(" \xB7 ")}`;
2521
5534
  }
5535
+ function renderAnimatedStatMd(slots, t) {
5536
+ const value = getSlotValue(slots, t, "value");
5537
+ const label = getSlotValue(slots, t, "label");
5538
+ const unit = getSlotValue(slots, t, "unit");
5539
+ const delta = getSlotValue(slots, t, "delta");
5540
+ const dir = getSlotValue(slots, t, "delta_direction");
5541
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
5542
+ return `**${value}${unit ? " " + unit : ""}** \u2014 ${label}${delta ? ` ${arrow} ${delta}` : ""}`;
5543
+ }
5544
+ function renderReleaseCardMd(slots, t) {
5545
+ const version = getSlotValue(slots, t, "version");
5546
+ const headline = getSlotValue(slots, t, "headline");
5547
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
5548
+ const tag = getSlotValue(slots, t, "tag");
5549
+ return `### ${version}${tag ? ` \`${tag}\`` : ""} \u2014 ${headline}
5550
+
5551
+ ${changes.map((c) => `- ${c}`).join("\n")}`;
5552
+ }
5553
+ function renderChartBarsMd(slots, t) {
5554
+ const title = getSlotValue(slots, t, "title");
5555
+ const labels = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
5556
+ const values = getSlotValue(slots, t, "values").split("\n").filter(Boolean);
5557
+ const rows = labels.map((l, i) => `- **${l}**: ${values[i] ?? "0"}`).join("\n");
5558
+ return `${title ? `## ${title}
5559
+
5560
+ ` : ""}${rows}`;
5561
+ }
5562
+ function renderArchitectureBadgeMd(slots, t) {
5563
+ const title = getSlotValue(slots, t, "title");
5564
+ const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
5565
+ const subtitle = getSlotValue(slots, t, "subtitle");
5566
+ return `${title ? `**${title}**
5567
+
5568
+ ` : ""}${items.map((i) => `- ${i.trim()}`).join("\n")}${subtitle ? `
5569
+
5570
+ *${subtitle}*` : ""}`;
5571
+ }
2522
5572
  var MD_FN = {
2523
5573
  "stat-card": renderStatCardMd,
2524
5574
  "feature-grid": renderFeatureGridMd,
@@ -2527,7 +5577,11 @@ var MD_FN = {
2527
5577
  "metric-badge": renderMetricBadgeMd,
2528
5578
  "pricing-tier": renderPricingTierMd,
2529
5579
  "changelog-row": renderChangelogRowMd,
2530
- "social-proof": renderSocialProofMd
5580
+ "social-proof": renderSocialProofMd,
5581
+ "animated-stat": renderAnimatedStatMd,
5582
+ "release-card": renderReleaseCardMd,
5583
+ "chart-bars": renderChartBarsMd,
5584
+ "architecture-badge": renderArchitectureBadgeMd
2531
5585
  };
2532
5586
  function toReactStyle(vars) {
2533
5587
  return "{\n" + Object.entries(vars).map(([k, v]) => ` "${k}": "${v.replace(/"/g, '\\"')}"`).join(",\n") + "\n }";
@@ -2559,6 +5613,89 @@ ${slotDecls}
2559
5613
  }
2560
5614
  `;
2561
5615
  }
5616
+ function getRequiredLibs(templateId) {
5617
+ const libs = /* @__PURE__ */ new Set(["tailwind"]);
5618
+ if (["animated-stat", "release-card"].includes(templateId)) {
5619
+ libs.add("anime");
5620
+ }
5621
+ if (templateId === "chart-bars") {
5622
+ libs.add("anime");
5623
+ libs.add("chartjs");
5624
+ }
5625
+ if (templateId === "architecture-badge") {
5626
+ libs.add("anime");
5627
+ libs.add("lucide");
5628
+ }
5629
+ return Array.from(libs);
5630
+ }
5631
+ function buildCdnScripts(libs) {
5632
+ const tags = [];
5633
+ if (libs.includes("tailwind")) {
5634
+ tags.push(`<script>window.tailwind={config:{},silent:true}</script>`);
5635
+ tags.push(`<script src="https://cdn.tailwindcss.com"></script>`);
5636
+ }
5637
+ if (libs.includes("chartjs")) {
5638
+ tags.push(`<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>`);
5639
+ }
5640
+ if (libs.includes("anime")) {
5641
+ tags.push(`<script src="https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js"></script>`);
5642
+ }
5643
+ if (libs.includes("lucide")) {
5644
+ tags.push(`<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>`);
5645
+ }
5646
+ if (libs.includes("alpine")) {
5647
+ tags.push(`<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>`);
5648
+ }
5649
+ return tags.join("\n ");
5650
+ }
5651
+ function renderStandalone(template, slots, brand, style) {
5652
+ const bodyFn = HTML_BODY_FN[template.id];
5653
+ const body = bodyFn ? bodyFn(slots, template) : "";
5654
+ const allVars = { ...brandVars(brand), ...styleVars(style) };
5655
+ const inlineStyle = buildInlineStyle(allVars);
5656
+ const customCss = style.customCss ?? "";
5657
+ const override = style.templateOverrides?.[template.id]?.html;
5658
+ const finalBody = override ?? body;
5659
+ const libs = getRequiredLibs(template.id);
5660
+ const scripts = buildCdnScripts(libs);
5661
+ const w = template.defaultWidth ?? 600;
5662
+ const h = template.defaultHeight ?? 400;
5663
+ return `<!DOCTYPE html>
5664
+ <html lang="en">
5665
+ <head>
5666
+ <meta charset="UTF-8" />
5667
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
5668
+ <title>${escHtml(template.name)} \u2014 forge0x2B</title>
5669
+ ${scripts}
5670
+ <style>
5671
+ *, *::before, *::after { box-sizing: border-box; }
5672
+ html, body {
5673
+ margin: 0; padding: 0;
5674
+ width: ${w}px; height: ${h}px;
5675
+ overflow: hidden;
5676
+ background: transparent;
5677
+ display: flex; align-items: center; justify-content: center;
5678
+ }
5679
+ .pw-widget {
5680
+ font-family: var(--pw-font-body);
5681
+ color: var(--pw-brand-text);
5682
+ background: var(--pw-brand-surface);
5683
+ border-radius: var(--pw-radius-md);
5684
+ padding: var(--pw-spacing-md);
5685
+ border: var(--pw-border-width) solid rgba(255,255,255,.08);
5686
+ box-sizing: border-box;
5687
+ width: 100%; max-width: ${w}px;
5688
+ ${customCss}
5689
+ }
5690
+ </style>
5691
+ </head>
5692
+ <body>
5693
+ <div class="pw-widget pw-${template.id}" style="${inlineStyle}">
5694
+ ${finalBody}
5695
+ </div>
5696
+ </body>
5697
+ </html>`;
5698
+ }
2562
5699
  function renderWidget(input) {
2563
5700
  const { template, slots, brandKit, format } = input;
2564
5701
  const style = !input.style ? STYLE_PRESET_DEFAULT : typeof input.style === "string" ? getStyleById(input.style) : input.style;
@@ -2569,6 +5706,9 @@ function renderWidget(input) {
2569
5706
  if (format === "react") {
2570
5707
  return renderReact(template, slots, brandKit ?? null, style);
2571
5708
  }
5709
+ if (format === "standalone") {
5710
+ return renderStandalone(template, slots, brandKit ?? null, style);
5711
+ }
2572
5712
  const bodyFn = HTML_BODY_FN[template.id];
2573
5713
  const body = bodyFn ? bodyFn(slots, template) : "";
2574
5714
  const allVars = { ...brandVars(brandKit ?? null), ...styleVars(style) };
@@ -2860,7 +6000,7 @@ function getDispatchChannel(id) {
2860
6000
  }
2861
6001
 
2862
6002
  // src/forge/dispatch.ts
2863
- function buildSystemPrompt7(channel, audience, brand, blueprintContext, toneOffset) {
6003
+ function buildSystemPrompt14(channel, audience, brand, blueprintContext, toneOffset) {
2864
6004
  const lines = [
2865
6005
  `You are a skilled content writer producing a ${channel.name} post.`,
2866
6006
  "",
@@ -2911,7 +6051,7 @@ function truncate(content, maxLength) {
2911
6051
  async function generateForChannel(ask, channel, provider, opts) {
2912
6052
  const now = (/* @__PURE__ */ new Date()).toISOString();
2913
6053
  try {
2914
- const system = buildSystemPrompt7(
6054
+ const system = buildSystemPrompt14(
2915
6055
  channel,
2916
6056
  opts.audience,
2917
6057
  opts.brand,
@@ -3056,7 +6196,7 @@ function nextVersionNumber(versions) {
3056
6196
  }
3057
6197
 
3058
6198
  // src/forge/refineAsset.ts
3059
- function buildSystemPrompt8(input) {
6199
+ function buildSystemPrompt15(input) {
3060
6200
  const parts = [
3061
6201
  `You are a content refinement assistant for a developer-focused content tool (forge0x2B).`,
3062
6202
  `You are refining an existing ${input.assetType.replace(/-/g, " ")} asset.`,
@@ -3095,7 +6235,7 @@ function parseRefinedResponse(raw) {
3095
6235
  return { newContent, llmReply };
3096
6236
  }
3097
6237
  async function refineAsset(input, provider) {
3098
- const systemPrompt = buildSystemPrompt8(input);
6238
+ const systemPrompt = buildSystemPrompt15(input);
3099
6239
  if (scanForSecrets(systemPrompt)) {
3100
6240
  throw new Error("refineAsset: secret pattern detected in asset content. Refusing to send to LLM.");
3101
6241
  }
@@ -3328,12 +6468,20 @@ exports.extractBrandFromPrismBlueprint = extractBrandFromPrismBlueprint;
3328
6468
  exports.extractDependencyHotspots = extractDependencyHotspots;
3329
6469
  exports.extractTopChurnFiles = extractTopChurnFiles;
3330
6470
  exports.extractZones = extractZones;
6471
+ exports.generateADR = generateADR;
6472
+ exports.generateArc42 = generateArc42;
3331
6473
  exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
3332
6474
  exports.generateAskDrivenAsset = generateAskDrivenAsset;
3333
6475
  exports.generateChangesSince = generateChangesSince;
6476
+ exports.generateComplianceDoc = generateComplianceDoc;
6477
+ exports.generateKnowledgeCapture = generateKnowledgeCapture;
6478
+ exports.generateNewsletter = generateNewsletter;
3334
6479
  exports.generateOnboardingDoc = generateOnboardingDoc;
6480
+ exports.generatePresentation = generatePresentation;
6481
+ exports.generateRadio = generateRadio;
3335
6482
  exports.generateRefactoringReport = generateRefactoringReport;
3336
6483
  exports.generateReleaseNotes = generateReleaseNotes;
6484
+ exports.generateSprintRetro = generateSprintRetro;
3337
6485
  exports.getDispatchChannel = getDispatchChannel;
3338
6486
  exports.getPrismTemplate = getPrismTemplate;
3339
6487
  exports.getSlotValue = getSlotValue;
@@ -3342,6 +6490,7 @@ exports.getWidgetTemplate = getWidgetTemplate;
3342
6490
  exports.initialFormValues = initialFormValues;
3343
6491
  exports.next7DaysRange = next7DaysRange;
3344
6492
  exports.nextVersionNumber = nextVersionNumber;
6493
+ exports.normalizePrismlensBlueprint = normalizePrismlensBlueprint;
3345
6494
  exports.orchestrateDispatch = orchestrateDispatch;
3346
6495
  exports.parseAndValidateSchemaDefinition = parseAndValidateSchemaDefinition;
3347
6496
  exports.parseBrandKitFromLlmResponse = parseBrandKitFromLlmResponse;
@@ -3351,8 +6500,10 @@ exports.parseStyleFromTailwindConfig = parseStyleFromTailwindConfig;
3351
6500
  exports.parseStyleFromTokensJson = parseStyleFromTokensJson;
3352
6501
  exports.parseThemeConfigContent = parseThemeConfigContent;
3353
6502
  exports.previewExport = previewExport;
6503
+ exports.readAmberLayer = readAmberLayer;
3354
6504
  exports.readBlueprintData = readBlueprintData;
3355
6505
  exports.readBlueprintFromTarget = readBlueprintFromTarget;
6506
+ exports.readGreenLayer = readGreenLayer;
3356
6507
  exports.readPrismDirectory = readPrismDirectory;
3357
6508
  exports.refineAsset = refineAsset;
3358
6509
  exports.refineLimitState = refineLimitState;