forgesmith 0.6.1 → 0.7.1
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/index.cjs +3531 -133
- package/dist/index.d.cts +625 -10
- package/dist/index.d.ts +625 -10
- package/dist/index.mjs +3521 -134
- package/dist/server.cjs +3902 -171
- package/dist/server.d.cts +655 -14
- package/dist/server.d.ts +655 -14
- package/dist/server.mjs +3885 -168
- package/package.json +1 -1
package/dist/server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
|
-
import
|
|
2
|
+
import path3, { join, normalize, resolve } from 'path';
|
|
3
3
|
import { existsSync, readFileSync } from 'fs';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ async function readJsonFiles(dir) {
|
|
|
11
11
|
for (const entry of entries) {
|
|
12
12
|
if (!entry.endsWith(".json")) continue;
|
|
13
13
|
try {
|
|
14
|
-
const raw = await fs.readFile(
|
|
14
|
+
const raw = await fs.readFile(path3.join(dir, entry), "utf-8");
|
|
15
15
|
results.push(JSON.parse(raw));
|
|
16
16
|
} catch {
|
|
17
17
|
}
|
|
@@ -22,9 +22,9 @@ async function readJsonFiles(dir) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
async function readPrismDirectory(prismPath) {
|
|
25
|
-
const sessionsDir =
|
|
26
|
-
const recsDir =
|
|
27
|
-
const insightsDir =
|
|
25
|
+
const sessionsDir = path3.join(prismPath, "sessions");
|
|
26
|
+
const recsDir = path3.join(prismPath, "recommendations");
|
|
27
|
+
const insightsDir = path3.join(prismPath, "green", "insights", "accepted");
|
|
28
28
|
const [sessions, recommendations, insights] = await Promise.all([
|
|
29
29
|
readJsonFiles(sessionsDir),
|
|
30
30
|
readJsonFiles(recsDir),
|
|
@@ -33,7 +33,7 @@ async function readPrismDirectory(prismPath) {
|
|
|
33
33
|
return { sessions, recommendations, insights };
|
|
34
34
|
}
|
|
35
35
|
async function readBlueprintData(targetPath) {
|
|
36
|
-
const snapshotPath =
|
|
36
|
+
const snapshotPath = path3.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
37
37
|
try {
|
|
38
38
|
const raw = await fs.readFile(snapshotPath, "utf-8");
|
|
39
39
|
return JSON.parse(raw);
|
|
@@ -41,6 +41,256 @@ async function readBlueprintData(targetPath) {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
+
function normalise(v, allowed, fallback) {
|
|
45
|
+
if (v && allowed.includes(v)) return v;
|
|
46
|
+
return fallback;
|
|
47
|
+
}
|
|
48
|
+
function parseCapabilitiesMd(content) {
|
|
49
|
+
const match = content.match(/^---\s*\n([\s\S]*?)\n---/m);
|
|
50
|
+
if (!match) return [];
|
|
51
|
+
const yaml = match[1];
|
|
52
|
+
const capsMatch = yaml.match(/^capabilities:\s*\n([\s\S]+)/m);
|
|
53
|
+
if (!capsMatch) return [];
|
|
54
|
+
const body = capsMatch[1];
|
|
55
|
+
const capabilities = [];
|
|
56
|
+
const items = body.split(/\n(?= - )/);
|
|
57
|
+
for (const item of items) {
|
|
58
|
+
if (!item.trim().startsWith("-")) continue;
|
|
59
|
+
const id = extractScalar(item, "id");
|
|
60
|
+
if (!id) continue;
|
|
61
|
+
const name = extractScalar(item, "name") ?? id;
|
|
62
|
+
const criticality = extractScalar(item, "criticality");
|
|
63
|
+
const lifecycle = extractScalar(item, "lifecycle");
|
|
64
|
+
const description = extractMultilineScalar(item, "description");
|
|
65
|
+
capabilities.push({
|
|
66
|
+
id,
|
|
67
|
+
name,
|
|
68
|
+
...criticality ? { criticality } : {},
|
|
69
|
+
...lifecycle ? { lifecycle } : {},
|
|
70
|
+
...description ? { description } : {}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return capabilities;
|
|
74
|
+
}
|
|
75
|
+
function extractScalar(block, key) {
|
|
76
|
+
const re = new RegExp(`\\n?\\s+${key}:\\s+(.+)`);
|
|
77
|
+
const m = block.match(re);
|
|
78
|
+
if (!m) return void 0;
|
|
79
|
+
return m[1].trim().replace(/^["']|["']$/g, "");
|
|
80
|
+
}
|
|
81
|
+
function extractMultilineScalar(block, key) {
|
|
82
|
+
const re = new RegExp(`\\n?\\s+${key}:\\s+>\\s*\\n((?:[ \\t]+.+\\n?)+)`);
|
|
83
|
+
const m = block.match(re);
|
|
84
|
+
if (!m) return extractScalar(block, key);
|
|
85
|
+
return m[1].split("\n").map((l) => l.trim()).filter(Boolean).join(" ");
|
|
86
|
+
}
|
|
87
|
+
async function readAmberLayer(targetPath) {
|
|
88
|
+
const amberDir = path3.join(targetPath, ".amber");
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(amberDir);
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
let rawCaps = [];
|
|
95
|
+
try {
|
|
96
|
+
const capsMd = await fs.readFile(
|
|
97
|
+
path3.join(amberDir, "capabilities.md"),
|
|
98
|
+
"utf-8"
|
|
99
|
+
);
|
|
100
|
+
rawCaps = parseCapabilitiesMd(capsMd);
|
|
101
|
+
} catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
if (rawCaps.length === 0) return null;
|
|
105
|
+
let state = null;
|
|
106
|
+
try {
|
|
107
|
+
const raw = await fs.readFile(path3.join(amberDir, "state.json"), "utf-8");
|
|
108
|
+
state = JSON.parse(raw);
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
const stateFiles = state?.files ?? {};
|
|
112
|
+
const allFilePaths = Object.keys(stateFiles);
|
|
113
|
+
const totalFiles = allFilePaths.length;
|
|
114
|
+
const capabilities = rawCaps.map((raw) => {
|
|
115
|
+
const tagged = allFilePaths.filter(
|
|
116
|
+
(fp) => stateFiles[fp]?.capabilities?.includes(raw.id)
|
|
117
|
+
);
|
|
118
|
+
const driftCount = tagged.filter(
|
|
119
|
+
(fp) => !stateFiles[fp]?.doc_hash
|
|
120
|
+
).length;
|
|
121
|
+
const hasTests = tagged.some((fp) => !!stateFiles[fp]?.doc_hash);
|
|
122
|
+
return {
|
|
123
|
+
id: raw.id,
|
|
124
|
+
name: raw.name,
|
|
125
|
+
criticality: normalise(raw.criticality, ["critical", "high", "medium", "low"], "medium"),
|
|
126
|
+
lifecycle: normalise(raw.lifecycle, ["active", "experimental", "deprecated", "sunset"], "active"),
|
|
127
|
+
...raw.description ? { description: raw.description } : {},
|
|
128
|
+
files: tagged,
|
|
129
|
+
driftCount,
|
|
130
|
+
hasTests
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
const taggedFilesSet = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const fp of allFilePaths) {
|
|
135
|
+
const entry = stateFiles[fp];
|
|
136
|
+
if (entry?.capabilities && entry.capabilities.length > 0) {
|
|
137
|
+
taggedFilesSet.add(fp);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const taggedFiles = taggedFilesSet.size;
|
|
141
|
+
const taggedPercent = totalFiles > 0 ? Math.round(taggedFiles / totalFiles * 100) : 0;
|
|
142
|
+
const orphanedFiles = allFilePaths.filter((fp) => {
|
|
143
|
+
const entry = stateFiles[fp];
|
|
144
|
+
return !entry?.capabilities || entry.capabilities.length === 0;
|
|
145
|
+
});
|
|
146
|
+
const driftedCapabilities = capabilities.filter((c) => c.driftCount > 0).length;
|
|
147
|
+
const criticalCaps = capabilities.filter((c) => c.criticality === "critical");
|
|
148
|
+
const highCaps = capabilities.filter((c) => c.criticality === "high");
|
|
149
|
+
const summaryParts = [
|
|
150
|
+
`${capabilities.length} business capabilities (${criticalCaps.length} critical, ${highCaps.length} high).`,
|
|
151
|
+
`${taggedFiles}/${totalFiles} files tagged (${taggedPercent}%).`
|
|
152
|
+
];
|
|
153
|
+
if (driftedCapabilities > 0) {
|
|
154
|
+
summaryParts.push(
|
|
155
|
+
`${driftedCapabilities} capabilit${driftedCapabilities === 1 ? "y has" : "ies have"} documentation drift.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (orphanedFiles.length > 0) {
|
|
159
|
+
summaryParts.push(`${orphanedFiles.length} files are untagged (orphaned).`);
|
|
160
|
+
}
|
|
161
|
+
const topCaps = capabilities.filter((c) => c.criticality === "critical" || c.criticality === "high").slice(0, 3).map((c) => c.name);
|
|
162
|
+
if (topCaps.length > 0) {
|
|
163
|
+
summaryParts.push(`Key capabilities: ${topCaps.join(", ")}.`);
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
capabilities,
|
|
167
|
+
totalFiles,
|
|
168
|
+
taggedFiles,
|
|
169
|
+
taggedPercent,
|
|
170
|
+
driftedCapabilities,
|
|
171
|
+
orphanedFiles,
|
|
172
|
+
summary: summaryParts.join(" ")
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
async function readJsonFile(filePath) {
|
|
176
|
+
try {
|
|
177
|
+
const raw = await fs.readFile(filePath, "utf-8");
|
|
178
|
+
return JSON.parse(raw);
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function dirExists(dirPath) {
|
|
184
|
+
try {
|
|
185
|
+
await fs.access(dirPath);
|
|
186
|
+
return true;
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function readGreenLayer(targetPath) {
|
|
192
|
+
const greenDir = path3.join(targetPath, ".prism", "green");
|
|
193
|
+
if (!await dirExists(greenDir)) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const history = await readJsonFile(
|
|
197
|
+
path3.join(greenDir, "coherence-history.json")
|
|
198
|
+
);
|
|
199
|
+
const latestScore = history?.latest ?? null;
|
|
200
|
+
const coherenceScore = latestScore?.score ?? null;
|
|
201
|
+
const coherenceGrade = latestScore?.grade ?? null;
|
|
202
|
+
const topRisks = (latestScore?.topRisks ?? []).map((r) => ({
|
|
203
|
+
capabilityId: r.capabilityId,
|
|
204
|
+
name: r.name,
|
|
205
|
+
reason: r.reason,
|
|
206
|
+
impact: r.impact
|
|
207
|
+
}));
|
|
208
|
+
const insightStatusMap = await readJsonFile(
|
|
209
|
+
path3.join(greenDir, "insight-status.json")
|
|
210
|
+
);
|
|
211
|
+
const proposedInsights = await readJsonFile(
|
|
212
|
+
path3.join(greenDir, "proposed-insights.json")
|
|
213
|
+
);
|
|
214
|
+
const insights = [];
|
|
215
|
+
if (Array.isArray(proposedInsights)) {
|
|
216
|
+
for (const raw of proposedInsights) {
|
|
217
|
+
const id = typeof raw.id === "string" ? raw.id : "";
|
|
218
|
+
if (!id) continue;
|
|
219
|
+
const status = insightStatusMap?.[id];
|
|
220
|
+
if (status === "rejected") continue;
|
|
221
|
+
const title = typeof raw.title === "string" ? raw.title : id;
|
|
222
|
+
const body = typeof raw.body === "string" ? raw.body : typeof raw.description === "string" ? raw.description : "";
|
|
223
|
+
const category = typeof raw.category === "string" ? raw.category : typeof raw.kind === "string" ? raw.kind : "general";
|
|
224
|
+
const severity = typeof raw.severity === "string" ? raw.severity : void 0;
|
|
225
|
+
insights.push({
|
|
226
|
+
id,
|
|
227
|
+
title,
|
|
228
|
+
body,
|
|
229
|
+
category,
|
|
230
|
+
...severity ? { severity } : {}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (insights.length === 0) {
|
|
235
|
+
const acceptedDir = path3.join(greenDir, "insights", "accepted");
|
|
236
|
+
if (await dirExists(acceptedDir)) {
|
|
237
|
+
try {
|
|
238
|
+
const entries = await fs.readdir(acceptedDir);
|
|
239
|
+
for (const entry of entries) {
|
|
240
|
+
if (!entry.endsWith(".json")) continue;
|
|
241
|
+
const raw = await readJsonFile(
|
|
242
|
+
path3.join(acceptedDir, entry)
|
|
243
|
+
);
|
|
244
|
+
if (!raw) continue;
|
|
245
|
+
const id = typeof raw.id === "string" ? raw.id : entry.replace(".json", "");
|
|
246
|
+
const title = typeof raw.title === "string" ? raw.title : id;
|
|
247
|
+
const body = typeof raw.body === "string" ? raw.body : typeof raw.description === "string" ? raw.description : "";
|
|
248
|
+
const category = typeof raw.category === "string" ? raw.category : typeof raw.kind === "string" ? raw.kind : "general";
|
|
249
|
+
const severity = typeof raw.severity === "string" ? raw.severity : void 0;
|
|
250
|
+
insights.push({
|
|
251
|
+
id,
|
|
252
|
+
title,
|
|
253
|
+
body,
|
|
254
|
+
category,
|
|
255
|
+
...severity ? { severity } : {}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (coherenceScore === null && insights.length === 0 && topRisks.length === 0) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const summaryParts = [];
|
|
266
|
+
if (coherenceScore !== null && coherenceGrade !== null) {
|
|
267
|
+
summaryParts.push(
|
|
268
|
+
`Architecture Coherence Score: ${coherenceScore}/100 (grade ${coherenceGrade}).`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (topRisks.length > 0) {
|
|
272
|
+
const topRiskNames = topRisks.slice(0, 3).map((r) => `"${r.name}" (${r.reason})`);
|
|
273
|
+
summaryParts.push(`Top risks: ${topRiskNames.join("; ")}.`);
|
|
274
|
+
}
|
|
275
|
+
if (insights.length > 0) {
|
|
276
|
+
const accepted = insights.filter(
|
|
277
|
+
(i) => insightStatusMap?.[i.id] === "accepted"
|
|
278
|
+
).length;
|
|
279
|
+
summaryParts.push(
|
|
280
|
+
`${insights.length} cross-layer insight${insights.length === 1 ? "" : "s"}` + (accepted > 0 ? ` (${accepted} accepted)` : "") + "."
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (summaryParts.length === 0) {
|
|
284
|
+
summaryParts.push("GREEN cross-layer analysis loaded but no actionable data found.");
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
insights,
|
|
288
|
+
coherenceScore,
|
|
289
|
+
coherenceGrade,
|
|
290
|
+
topRisks,
|
|
291
|
+
summary: summaryParts.join(" ")
|
|
292
|
+
};
|
|
293
|
+
}
|
|
44
294
|
|
|
45
295
|
// src/forge/secrets.ts
|
|
46
296
|
var SECRET_PATTERNS = [
|
|
@@ -54,12 +304,86 @@ function scanForSecrets(text) {
|
|
|
54
304
|
}
|
|
55
305
|
|
|
56
306
|
// src/forge/blueprintReader.ts
|
|
307
|
+
function isPrismlensShape(raw) {
|
|
308
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
309
|
+
const r = raw;
|
|
310
|
+
return "generatedAt" in r || "nodes" in r && !("files" in r);
|
|
311
|
+
}
|
|
312
|
+
function normalizePrismlensBlueprint(raw, targetPath = "") {
|
|
313
|
+
if (!isPrismlensShape(raw)) {
|
|
314
|
+
return raw;
|
|
315
|
+
}
|
|
316
|
+
const p = raw;
|
|
317
|
+
const summary = p.summary ?? {};
|
|
318
|
+
const importedByCounts = /* @__PURE__ */ new Map();
|
|
319
|
+
if (Array.isArray(summary.mostImportedFiles)) {
|
|
320
|
+
for (const entry of summary.mostImportedFiles) {
|
|
321
|
+
if (entry && typeof entry.path === "string" && typeof entry.count === "number") {
|
|
322
|
+
importedByCounts.set(entry.path, entry.count);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const nodePathById = /* @__PURE__ */ new Map();
|
|
327
|
+
const nodes = Array.isArray(p.nodes) ? p.nodes : [];
|
|
328
|
+
for (const node of nodes) {
|
|
329
|
+
if (node.id && node.path) nodePathById.set(node.id, node.path);
|
|
330
|
+
}
|
|
331
|
+
const importCountByPath = /* @__PURE__ */ new Map();
|
|
332
|
+
const prismlensEdges = Array.isArray(p.edges) ? p.edges : [];
|
|
333
|
+
for (const edge of prismlensEdges) {
|
|
334
|
+
const sourcePath = nodePathById.get(edge.source);
|
|
335
|
+
if (sourcePath) {
|
|
336
|
+
importCountByPath.set(sourcePath, (importCountByPath.get(sourcePath) ?? 0) + 1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const files = nodes.map((node) => ({
|
|
340
|
+
path: node.path,
|
|
341
|
+
category: node.category ?? "unknown",
|
|
342
|
+
importCount: importCountByPath.get(node.path) ?? 0,
|
|
343
|
+
importedByCount: importedByCounts.get(node.path) ?? 0,
|
|
344
|
+
lineCount: node.loc ?? 0
|
|
345
|
+
}));
|
|
346
|
+
const edges = prismlensEdges.map((e) => ({
|
|
347
|
+
from: nodePathById.get(e.source) ?? e.source,
|
|
348
|
+
to: nodePathById.get(e.target) ?? e.target,
|
|
349
|
+
type: e.type
|
|
350
|
+
}));
|
|
351
|
+
const categories = {
|
|
352
|
+
app: 0,
|
|
353
|
+
component: 0,
|
|
354
|
+
lib: 0,
|
|
355
|
+
hook: 0
|
|
356
|
+
};
|
|
357
|
+
for (const f of files) {
|
|
358
|
+
const cat = f.category ?? "unknown";
|
|
359
|
+
categories[cat] = (categories[cat] ?? 0) + 1;
|
|
360
|
+
}
|
|
361
|
+
if (typeof summary.componentCount === "number") categories.component = summary.componentCount;
|
|
362
|
+
if (typeof summary.libCount === "number") categories.lib = summary.libCount;
|
|
363
|
+
if (typeof summary.hookCount === "number") categories.hook = summary.hookCount;
|
|
364
|
+
if (typeof summary.pageCount === "number") categories.app = summary.pageCount;
|
|
365
|
+
const totalFiles = summary.fileCount ?? files.length;
|
|
366
|
+
const runtimeEdges = summary.runtimeInternalDependencyCount ?? summary.edgeCount ?? edges.length;
|
|
367
|
+
let scanTimestamp = Math.floor(Date.now() / 1e3);
|
|
368
|
+
if (typeof p.generatedAt === "string") {
|
|
369
|
+
const parsed = Date.parse(p.generatedAt);
|
|
370
|
+
if (!Number.isNaN(parsed)) scanTimestamp = Math.floor(parsed / 1e3);
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
targetPath,
|
|
374
|
+
scanTimestamp,
|
|
375
|
+
stats: { totalFiles, runtimeEdges },
|
|
376
|
+
files,
|
|
377
|
+
edges,
|
|
378
|
+
categories
|
|
379
|
+
};
|
|
380
|
+
}
|
|
57
381
|
var ALLOWED_BLUEPRINT_FILENAMES = /* @__PURE__ */ new Set(["blueprint.json", "snapshot.json"]);
|
|
58
382
|
function isAllowedBlueprintPath(targetPath, filePath) {
|
|
59
383
|
const resolvedTarget = resolve(targetPath);
|
|
60
384
|
const resolvedFile = resolve(filePath);
|
|
61
|
-
const
|
|
62
|
-
if (!resolvedFile.startsWith(
|
|
385
|
+
const prismRoot = join(resolvedTarget, ".prism");
|
|
386
|
+
if (!resolvedFile.startsWith(prismRoot + "/") && resolvedFile !== prismRoot) {
|
|
63
387
|
return false;
|
|
64
388
|
}
|
|
65
389
|
const basename = resolvedFile.slice(resolvedFile.lastIndexOf("/") + 1);
|
|
@@ -67,16 +391,22 @@ function isAllowedBlueprintPath(targetPath, filePath) {
|
|
|
67
391
|
}
|
|
68
392
|
function readBlueprintFromTarget(targetPath) {
|
|
69
393
|
const candidates = [
|
|
70
|
-
join(targetPath, ".prism", "blueprint", "snapshot.json"),
|
|
71
|
-
join(targetPath, ".prism", "blueprint.json")
|
|
394
|
+
{ path: join(targetPath, ".prism", "blueprint", "snapshot.json"), isPrismlens: false },
|
|
395
|
+
{ path: join(targetPath, ".prism", "blueprint.json"), isPrismlens: true }
|
|
72
396
|
];
|
|
73
|
-
for (const candidate of candidates) {
|
|
397
|
+
for (const { path: candidate, isPrismlens } of candidates) {
|
|
74
398
|
const normalized = normalize(candidate);
|
|
75
399
|
if (!isAllowedBlueprintPath(targetPath, normalized)) continue;
|
|
76
400
|
if (!existsSync(normalized)) continue;
|
|
77
401
|
try {
|
|
78
402
|
const raw = readFileSync(normalized, "utf-8");
|
|
79
|
-
|
|
403
|
+
const parsed = JSON.parse(raw);
|
|
404
|
+
if (isPrismlens || isPrismlensShape(parsed)) {
|
|
405
|
+
console.log(`[forgesmith] blueprint: reading prismlens shape from ${normalized}`);
|
|
406
|
+
return normalizePrismlensBlueprint(parsed, targetPath);
|
|
407
|
+
}
|
|
408
|
+
console.log(`[forgesmith] blueprint: reading native snapshot from ${normalized}`);
|
|
409
|
+
return parsed;
|
|
80
410
|
} catch {
|
|
81
411
|
return null;
|
|
82
412
|
}
|
|
@@ -171,7 +501,7 @@ function deriveAudienceMix(categories, paths) {
|
|
|
171
501
|
return "developer+business";
|
|
172
502
|
}
|
|
173
503
|
async function extractBrandFromPrismBlueprint(targetPath) {
|
|
174
|
-
const snapshotPath =
|
|
504
|
+
const snapshotPath = path3.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
175
505
|
let blueprint = {};
|
|
176
506
|
try {
|
|
177
507
|
const raw = await fs.readFile(snapshotPath, "utf-8");
|
|
@@ -184,7 +514,7 @@ async function extractBrandFromPrismBlueprint(targetPath) {
|
|
|
184
514
|
const total = blueprint.stats?.totalFiles ?? files.length;
|
|
185
515
|
const technicalScore = deriveTechnicalScore(categories, total);
|
|
186
516
|
const audienceMix = deriveAudienceMix(categories, paths);
|
|
187
|
-
const projectName = blueprint.targetPath ?
|
|
517
|
+
const projectName = blueprint.targetPath ? path3.basename(blueprint.targetPath) : "Project";
|
|
188
518
|
const formality = technicalScore > 60 ? 65 : 50;
|
|
189
519
|
const technicality = Math.min(95, technicalScore + 10);
|
|
190
520
|
return {
|
|
@@ -365,12 +695,12 @@ User wants to highlight: ${highlight}`;
|
|
|
365
695
|
function buildSystemPrompt() {
|
|
366
696
|
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.`;
|
|
367
697
|
}
|
|
368
|
-
function buildUserPrompt(data, opts) {
|
|
698
|
+
function buildUserPrompt(data, opts, amberContext, greenContext) {
|
|
369
699
|
const tone = opts.tone ?? "professional";
|
|
370
700
|
const length = opts.length ?? "medium";
|
|
371
701
|
const format = opts.format ?? "markdown";
|
|
372
702
|
const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
|
|
373
|
-
const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
|
|
703
|
+
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";
|
|
374
704
|
const hasSessions = (data.sessions?.length ?? 0) > 0;
|
|
375
705
|
const hasRecs = (data.recommendations?.length ?? 0) > 0;
|
|
376
706
|
const hasInsights = (data.insights?.length ?? 0) > 0;
|
|
@@ -406,11 +736,38 @@ function buildUserPrompt(data, opts) {
|
|
|
406
736
|
}
|
|
407
737
|
lines.push(``);
|
|
408
738
|
}
|
|
739
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
740
|
+
lines.push(`## Business Capabilities (AMBER layer)`);
|
|
741
|
+
lines.push(amberContext.summary);
|
|
742
|
+
const criticalCaps = amberContext.capabilities.filter(
|
|
743
|
+
(c) => c.criticality === "critical" || c.criticality === "high"
|
|
744
|
+
);
|
|
745
|
+
if (criticalCaps.length > 0) {
|
|
746
|
+
lines.push(
|
|
747
|
+
`This release may touch these capabilities: ${criticalCaps.map((c) => c.name).join(", ")}.`
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
lines.push(``);
|
|
751
|
+
}
|
|
752
|
+
if (greenContext) {
|
|
753
|
+
if (greenContext.coherenceScore !== null) {
|
|
754
|
+
lines.push(`## Architecture Health (GREEN layer)`);
|
|
755
|
+
lines.push(
|
|
756
|
+
`Coherence score: ${greenContext.coherenceScore}/100 (grade ${greenContext.coherenceGrade ?? "?"}).`
|
|
757
|
+
);
|
|
758
|
+
if (greenContext.topRisks.length > 0) {
|
|
759
|
+
lines.push(
|
|
760
|
+
`Top risks: ${greenContext.topRisks.slice(0, 3).map((r) => `${r.name} \u2014 ${r.reason}`).join("; ")}.`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
lines.push(``);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
409
766
|
return lines.join("\n");
|
|
410
767
|
}
|
|
411
|
-
async function generateReleaseNotes(data, opts, provider) {
|
|
768
|
+
async function generateReleaseNotes(data, opts, provider, amberContext, greenContext) {
|
|
412
769
|
const systemPrompt = buildSystemPrompt();
|
|
413
|
-
const userPrompt = buildUserPrompt(data, opts);
|
|
770
|
+
const userPrompt = buildUserPrompt(data, opts, amberContext, greenContext);
|
|
414
771
|
const response = await provider.complete({
|
|
415
772
|
systemPrompt,
|
|
416
773
|
messages: [{ role: "user", content: userPrompt }],
|
|
@@ -430,13 +787,16 @@ async function generateReleaseNotes(data, opts, provider) {
|
|
|
430
787
|
var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
|
|
431
788
|
function buildSystemPrompt2(tone) {
|
|
432
789
|
const toneMap = {
|
|
433
|
-
|
|
790
|
+
professional: "You are a senior software architect who writes clear, professional documentation suitable for all engineering audiences.",
|
|
434
791
|
casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
|
|
792
|
+
technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
|
|
793
|
+
executive: "You are a CTO-level advisor who explains codebase architecture at a high strategic level for leadership audiences.",
|
|
794
|
+
// Legacy aliases kept for backwards compatibility
|
|
435
795
|
onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
|
|
436
796
|
};
|
|
437
|
-
return `${toneMap[tone] ?? toneMap.
|
|
797
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the requested document \u2014 no preamble, no meta-commentary.`;
|
|
438
798
|
}
|
|
439
|
-
function buildUserPrompt2(blueprint, opts) {
|
|
799
|
+
function buildUserPrompt2(blueprint, opts, amberContext) {
|
|
440
800
|
const tone = opts.tone ?? "technical";
|
|
441
801
|
const length = opts.length ?? "long";
|
|
442
802
|
const format = opts.format ?? "markdown";
|
|
@@ -464,9 +824,28 @@ function buildUserPrompt2(blueprint, opts) {
|
|
|
464
824
|
``,
|
|
465
825
|
`Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
|
|
466
826
|
];
|
|
827
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
828
|
+
lines.push(``);
|
|
829
|
+
lines.push(`## Business Capabilities (AMBER layer)`);
|
|
830
|
+
lines.push(amberContext.summary);
|
|
831
|
+
lines.push(``);
|
|
832
|
+
const sorted = amberContext.capabilities.slice().sort((a, b) => {
|
|
833
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
834
|
+
return (rank[a.criticality] ?? 4) - (rank[b.criticality] ?? 4);
|
|
835
|
+
});
|
|
836
|
+
for (const cap of sorted) {
|
|
837
|
+
const tags = [cap.criticality, cap.lifecycle].filter(Boolean).join(", ");
|
|
838
|
+
const fileCount = cap.files.length;
|
|
839
|
+
lines.push(
|
|
840
|
+
`- **${cap.name}** (${cap.id}) [${tags}]${fileCount > 0 ? ` \u2014 ${fileCount} file${fileCount === 1 ? "" : "s"}` : ""}`
|
|
841
|
+
);
|
|
842
|
+
if (cap.description) lines.push(` ${cap.description}`);
|
|
843
|
+
}
|
|
844
|
+
lines.push(`Include a "Business Capabilities" section in the walkthrough mapping these capabilities to the codebase.`);
|
|
845
|
+
}
|
|
467
846
|
return lines.join("\n");
|
|
468
847
|
}
|
|
469
|
-
async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
848
|
+
async function generateArchitectureWalkthrough(blueprint, opts, provider, amberContext) {
|
|
470
849
|
if (!blueprint) {
|
|
471
850
|
return {
|
|
472
851
|
text: NO_DATA_MSG,
|
|
@@ -475,7 +854,7 @@ async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
|
475
854
|
}
|
|
476
855
|
const response = await provider.complete({
|
|
477
856
|
systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
|
|
478
|
-
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
|
|
857
|
+
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts, amberContext) }],
|
|
479
858
|
maxTokens: 4096
|
|
480
859
|
});
|
|
481
860
|
return {
|
|
@@ -550,11 +929,15 @@ async function generateChangesSince(current, previous, opts, provider) {
|
|
|
550
929
|
var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
|
|
551
930
|
function buildSystemPrompt4(tone) {
|
|
552
931
|
const toneMap = {
|
|
932
|
+
professional: "You are a documentation engineer writing a structured, professional onboarding reference for engineering teams.",
|
|
933
|
+
casual: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
934
|
+
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals.",
|
|
935
|
+
executive: "You are a technical lead writing a concise onboarding overview suitable for both technical and leadership audiences.",
|
|
936
|
+
// Legacy aliases kept for backwards compatibility
|
|
553
937
|
friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
554
|
-
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams."
|
|
555
|
-
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
|
|
938
|
+
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams."
|
|
556
939
|
};
|
|
557
|
-
return `${toneMap[tone] ?? toneMap.
|
|
940
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
558
941
|
}
|
|
559
942
|
function buildUserPrompt4(blueprint, opts) {
|
|
560
943
|
const tone = opts.tone ?? "friendly";
|
|
@@ -615,11 +998,14 @@ async function generateOnboardingDoc(blueprint, opts, provider) {
|
|
|
615
998
|
var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
|
|
616
999
|
function buildSystemPrompt5(tone) {
|
|
617
1000
|
const toneMap = {
|
|
618
|
-
|
|
1001
|
+
professional: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
|
|
619
1002
|
casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
|
|
620
|
-
|
|
1003
|
+
technical: "You are a senior engineer who produces detailed, implementation-focused refactoring analysis with concrete code-level recommendations.",
|
|
1004
|
+
executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership.",
|
|
1005
|
+
// Legacy aliases kept for backwards compatibility
|
|
1006
|
+
analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization."
|
|
621
1007
|
};
|
|
622
|
-
return `${toneMap[tone] ?? toneMap.
|
|
1008
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the report \u2014 no preamble, no meta-commentary.`;
|
|
623
1009
|
}
|
|
624
1010
|
function detectImportCycles(edges) {
|
|
625
1011
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -630,12 +1016,12 @@ function detectImportCycles(edges) {
|
|
|
630
1016
|
const cycles = [];
|
|
631
1017
|
const visited = /* @__PURE__ */ new Set();
|
|
632
1018
|
const stack = /* @__PURE__ */ new Set();
|
|
633
|
-
function dfs(node,
|
|
1019
|
+
function dfs(node, path5) {
|
|
634
1020
|
if (cycles.length >= 5) return;
|
|
635
1021
|
if (stack.has(node)) {
|
|
636
|
-
const cycleStart =
|
|
1022
|
+
const cycleStart = path5.indexOf(node);
|
|
637
1023
|
if (cycleStart !== -1) {
|
|
638
|
-
cycles.push(
|
|
1024
|
+
cycles.push(path5.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
|
|
639
1025
|
}
|
|
640
1026
|
return;
|
|
641
1027
|
}
|
|
@@ -643,142 +1029,3085 @@ function detectImportCycles(edges) {
|
|
|
643
1029
|
visited.add(node);
|
|
644
1030
|
stack.add(node);
|
|
645
1031
|
for (const neighbor of graph.get(node) ?? []) {
|
|
646
|
-
dfs(neighbor, [...
|
|
1032
|
+
dfs(neighbor, [...path5, node]);
|
|
1033
|
+
}
|
|
1034
|
+
stack.delete(node);
|
|
1035
|
+
}
|
|
1036
|
+
for (const node of graph.keys()) {
|
|
1037
|
+
if (!visited.has(node)) dfs(node, []);
|
|
1038
|
+
}
|
|
1039
|
+
return cycles;
|
|
1040
|
+
}
|
|
1041
|
+
function buildUserPrompt5(blueprint, opts, amberContext, greenContext) {
|
|
1042
|
+
const tone = opts.tone ?? "analytical";
|
|
1043
|
+
const length = opts.length ?? "medium";
|
|
1044
|
+
const format = opts.format ?? "markdown";
|
|
1045
|
+
const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
|
|
1046
|
+
const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
|
|
1047
|
+
const importCycles = detectImportCycles(blueprint.edges);
|
|
1048
|
+
const lines = [
|
|
1049
|
+
`Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
|
|
1050
|
+
`Tone: ${tone}. Length: ${length}.`,
|
|
1051
|
+
``,
|
|
1052
|
+
`## Architecture Metrics`,
|
|
1053
|
+
`- Total files: ${blueprint.stats.totalFiles}`,
|
|
1054
|
+
`- Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1055
|
+
`- Categories: ${JSON.stringify(blueprint.categories)}`,
|
|
1056
|
+
``,
|
|
1057
|
+
`## High-Coupling Hot Spots (high total connections)`,
|
|
1058
|
+
...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
1059
|
+
``,
|
|
1060
|
+
`## High Fan-Out Files (many outgoing dependencies)`,
|
|
1061
|
+
...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
|
|
1062
|
+
``,
|
|
1063
|
+
`## Import Cycles Detected (${importCycles.length})`,
|
|
1064
|
+
...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
|
|
1065
|
+
``,
|
|
1066
|
+
`Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
|
|
1067
|
+
];
|
|
1068
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
1069
|
+
lines.push(``);
|
|
1070
|
+
lines.push(`## Business Capabilities with Documentation Drift (AMBER layer)`);
|
|
1071
|
+
lines.push(amberContext.summary);
|
|
1072
|
+
const drifted = amberContext.capabilities.filter((c) => c.driftCount > 0);
|
|
1073
|
+
if (drifted.length > 0) {
|
|
1074
|
+
lines.push(`Capabilities with drift (files changed without updating @amber-doc):`);
|
|
1075
|
+
for (const cap of drifted) {
|
|
1076
|
+
lines.push(
|
|
1077
|
+
`- **${cap.name}** (${cap.id}, ${cap.criticality}): ${cap.driftCount} file${cap.driftCount === 1 ? "" : "s"} drifted`
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (greenContext) {
|
|
1083
|
+
lines.push(``);
|
|
1084
|
+
lines.push(`## Cross-Layer Risks (GREEN layer)`);
|
|
1085
|
+
lines.push(greenContext.summary);
|
|
1086
|
+
if (greenContext.topRisks.length > 0) {
|
|
1087
|
+
lines.push(`Top architectural risks:`);
|
|
1088
|
+
for (const risk of greenContext.topRisks) {
|
|
1089
|
+
lines.push(
|
|
1090
|
+
`- **${risk.name}** (impact: ${risk.impact}): ${risk.reason}`
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (greenContext.insights.length > 0) {
|
|
1095
|
+
lines.push(`Cross-layer insights:`);
|
|
1096
|
+
for (const insight of greenContext.insights.slice(0, 5)) {
|
|
1097
|
+
lines.push(`- **${insight.title}**: ${insight.body}`);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return lines.join("\n");
|
|
1102
|
+
}
|
|
1103
|
+
async function generateRefactoringReport(blueprint, opts, provider, amberContext, greenContext) {
|
|
1104
|
+
if (!blueprint) {
|
|
1105
|
+
return {
|
|
1106
|
+
text: NO_DATA_MSG4,
|
|
1107
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
const response = await provider.complete({
|
|
1111
|
+
systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
|
|
1112
|
+
messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts, amberContext, greenContext) }],
|
|
1113
|
+
maxTokens: 3072
|
|
1114
|
+
});
|
|
1115
|
+
return {
|
|
1116
|
+
text: response.content,
|
|
1117
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/generators/askDrivenAsset.ts
|
|
1122
|
+
var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
|
|
1123
|
+
var FORMAT_GUIDES = {
|
|
1124
|
+
markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
|
|
1125
|
+
blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
|
|
1126
|
+
social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
|
|
1127
|
+
email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
|
|
1128
|
+
slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
|
|
1129
|
+
slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
|
|
1130
|
+
};
|
|
1131
|
+
function buildSystemPrompt6(format, tone) {
|
|
1132
|
+
const toneMap = {
|
|
1133
|
+
professional: "You are a professional technical writer and developer advocate.",
|
|
1134
|
+
casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
|
|
1135
|
+
technical: "You are a senior software engineer writing precise, implementation-focused content.",
|
|
1136
|
+
executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
|
|
1137
|
+
};
|
|
1138
|
+
const guide = FORMAT_GUIDES[format];
|
|
1139
|
+
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.`;
|
|
1140
|
+
}
|
|
1141
|
+
function buildUserPrompt6(blueprint, question, opts) {
|
|
1142
|
+
const format = opts.format ?? "markdown";
|
|
1143
|
+
const tone = opts.tone ?? "professional";
|
|
1144
|
+
const length = opts.length ?? "medium";
|
|
1145
|
+
const guide = FORMAT_GUIDES[format];
|
|
1146
|
+
const lengthGuide = {
|
|
1147
|
+
short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
|
|
1148
|
+
medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
|
|
1149
|
+
long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
|
|
1150
|
+
}[length];
|
|
1151
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
1152
|
+
const lines = [
|
|
1153
|
+
`## User's Question`,
|
|
1154
|
+
question,
|
|
1155
|
+
``,
|
|
1156
|
+
`## Output Requirements`,
|
|
1157
|
+
`- Format: ${guide.name}`,
|
|
1158
|
+
`- Tone: ${tone}`,
|
|
1159
|
+
`- Length: ${length} (${lengthGuide})`,
|
|
1160
|
+
``,
|
|
1161
|
+
`## Codebase Architecture Context`,
|
|
1162
|
+
`Target: ${blueprint.targetPath}`,
|
|
1163
|
+
`Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1164
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
1165
|
+
``,
|
|
1166
|
+
`Key files (by usage):`,
|
|
1167
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
|
|
1168
|
+
];
|
|
1169
|
+
if (blueprint.edges.length > 0) {
|
|
1170
|
+
const edgeSample = blueprint.edges.slice(0, 15);
|
|
1171
|
+
lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
|
|
1172
|
+
}
|
|
1173
|
+
lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
|
|
1174
|
+
return lines.join("\n");
|
|
1175
|
+
}
|
|
1176
|
+
async function generateAskDrivenAsset(blueprint, question, opts, provider) {
|
|
1177
|
+
if (!blueprint) {
|
|
1178
|
+
return {
|
|
1179
|
+
text: NO_DATA_MSG5,
|
|
1180
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
if (!question || !question.trim()) {
|
|
1184
|
+
return {
|
|
1185
|
+
text: "No question provided. Please ask something about your codebase.",
|
|
1186
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
const format = opts.format ?? "markdown";
|
|
1190
|
+
const tone = opts.tone ?? "professional";
|
|
1191
|
+
const maxTokens = FORMAT_GUIDES[format].maxTokens;
|
|
1192
|
+
const response = await provider.complete({
|
|
1193
|
+
systemPrompt: buildSystemPrompt6(format, tone),
|
|
1194
|
+
messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
|
|
1195
|
+
maxTokens
|
|
1196
|
+
});
|
|
1197
|
+
return {
|
|
1198
|
+
text: response.content,
|
|
1199
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/generators/generatePresentation.ts
|
|
1204
|
+
function buildContext(blueprint, opts) {
|
|
1205
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
|
|
1206
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
1207
|
+
const totalLoc = blueprint.files.reduce(
|
|
1208
|
+
(sum, f) => sum + (f.lineCount ?? 0),
|
|
1209
|
+
0
|
|
1210
|
+
);
|
|
1211
|
+
const lines = [
|
|
1212
|
+
`## Codebase Context`,
|
|
1213
|
+
`Project: ${projectName}`,
|
|
1214
|
+
`Path: ${blueprint.targetPath}`,
|
|
1215
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
1216
|
+
`Total lines of code: ${totalLoc}`,
|
|
1217
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1218
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
1219
|
+
``,
|
|
1220
|
+
`## Most-imported files`,
|
|
1221
|
+
...topFiles.map(
|
|
1222
|
+
(f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, lines: ${f.lineCount ?? "?"}`
|
|
1223
|
+
)
|
|
1224
|
+
];
|
|
1225
|
+
if (blueprint.edges.length > 0) {
|
|
1226
|
+
const edgeSample = blueprint.edges.slice(0, 10);
|
|
1227
|
+
lines.push(
|
|
1228
|
+
``,
|
|
1229
|
+
`## Dependency sample`,
|
|
1230
|
+
...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`)
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
return lines.join("\n");
|
|
1234
|
+
}
|
|
1235
|
+
function planSlideTypes(opts) {
|
|
1236
|
+
const { slideCount, focusArea, audience } = opts;
|
|
1237
|
+
const focus = focusArea ?? "all";
|
|
1238
|
+
const always = ["title", "closing"];
|
|
1239
|
+
let middle = [];
|
|
1240
|
+
if (focus === "architecture") {
|
|
1241
|
+
middle = ["executive", "architecture", "metrics", "risks", "recommendations"];
|
|
1242
|
+
} else if (focus === "health") {
|
|
1243
|
+
middle = ["executive", "health", "risks", "recommendations", "metrics"];
|
|
1244
|
+
} else if (focus === "risks") {
|
|
1245
|
+
middle = ["executive", "risks", "recommendations", "architecture", "health"];
|
|
1246
|
+
} else {
|
|
1247
|
+
if (audience === "executive") {
|
|
1248
|
+
middle = ["executive", "health", "risks", "recommendations", "architecture", "metrics", "capabilities", "recommendations"];
|
|
1249
|
+
} else if (audience === "technical") {
|
|
1250
|
+
middle = ["architecture", "metrics", "risks", "health", "recommendations", "capabilities", "executive", "recommendations"];
|
|
1251
|
+
} else {
|
|
1252
|
+
middle = ["executive", "architecture", "health", "risks", "recommendations", "metrics", "capabilities", "executive"];
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const targetMiddle = slideCount - 2;
|
|
1256
|
+
const trimmed = middle.slice(0, targetMiddle);
|
|
1257
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1258
|
+
const deduped = [];
|
|
1259
|
+
for (const t of trimmed) {
|
|
1260
|
+
if (!seen.has(t)) {
|
|
1261
|
+
seen.add(t);
|
|
1262
|
+
deduped.push(t);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return [always[0], ...deduped, always[1]];
|
|
1266
|
+
}
|
|
1267
|
+
function buildSystemPrompt7(opts) {
|
|
1268
|
+
const toneMap = {
|
|
1269
|
+
professional: "You are a senior engineering consultant who creates compelling, data-driven presentations for technical and business stakeholders.",
|
|
1270
|
+
casual: "You are a developer advocate who creates friendly, approachable presentations that make architecture exciting.",
|
|
1271
|
+
executive: "You are a VP of Engineering who creates concise, business-value-focused presentations for C-suite audiences."
|
|
1272
|
+
};
|
|
1273
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
1274
|
+
}
|
|
1275
|
+
function buildUserPrompt7(blueprint, opts, slideTypes) {
|
|
1276
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
1277
|
+
const context = buildContext(blueprint, opts);
|
|
1278
|
+
const slideTypeDescriptions = {
|
|
1279
|
+
title: "Opening slide with project name, tagline, and date. Fields: title (project name), subtitle (tagline), speakerNotes",
|
|
1280
|
+
executive: "Executive summary with 3 key findings. Fields: title, bullets (exactly 3 bullet strings), speakerNotes",
|
|
1281
|
+
architecture: "Architecture overview. Fields: title, subtitle (layer summary), bullets (up to 5 key architecture facts), visualHint (describe a diagram), speakerNotes",
|
|
1282
|
+
capabilities: "Capability map overview. Fields: title, bullets (up to 5 capability areas found), speakerNotes",
|
|
1283
|
+
health: "Codebase health score. Fields: title, highlight (a letter grade A-F or numeric score), highlightLabel, bullets (up to 3 health indicators), speakerNotes",
|
|
1284
|
+
risks: "Top risks. Fields: title, bullets (exactly 3 risks, each prefixed with High/Medium/Low impact), speakerNotes",
|
|
1285
|
+
recommendations: "Action items. Fields: title, bullets (up to 5 concrete action items), speakerNotes",
|
|
1286
|
+
metrics: "Key metrics. Fields: title, highlight (most important metric), highlightLabel, bullets (up to 5 metrics), speakerNotes",
|
|
1287
|
+
closing: "Next steps and call to action. Fields: title, subtitle, bullets (up to 3 next steps), speakerNotes"
|
|
1288
|
+
};
|
|
1289
|
+
const slidePlan = slideTypes.map((t, i) => ` ${i + 1}. type="${t}" \u2014 ${slideTypeDescriptions[t]}`).join("\n");
|
|
1290
|
+
return [
|
|
1291
|
+
`Generate a ${opts.slideCount}-slide presentation deck for: ${projectName}`,
|
|
1292
|
+
`Audience: ${opts.audience}. Tone: ${opts.tone}. Theme: ${opts.theme}.`,
|
|
1293
|
+
``,
|
|
1294
|
+
context,
|
|
1295
|
+
``,
|
|
1296
|
+
`## Slide Plan (${slideTypes.length} slides)`,
|
|
1297
|
+
slidePlan,
|
|
1298
|
+
``,
|
|
1299
|
+
`## Output Format`,
|
|
1300
|
+
`Return a single JSON object with this exact shape:`,
|
|
1301
|
+
`{`,
|
|
1302
|
+
` "title": "<deck title>",`,
|
|
1303
|
+
` "subtitle": "<deck subtitle>",`,
|
|
1304
|
+
` "slides": [`,
|
|
1305
|
+
` {`,
|
|
1306
|
+
` "type": "<one of the PresentationSlideType values>",`,
|
|
1307
|
+
` "title": "<slide title>",`,
|
|
1308
|
+
` "subtitle": "<optional subtitle>",`,
|
|
1309
|
+
` "bullets": ["<bullet 1>", "<bullet 2>"],`,
|
|
1310
|
+
` "highlight": "<optional large callout>",`,
|
|
1311
|
+
` "highlightLabel": "<optional label>",`,
|
|
1312
|
+
` "speakerNotes": "<presenter notes>",`,
|
|
1313
|
+
` "visualHint": "<optional visual description>"`,
|
|
1314
|
+
` }`,
|
|
1315
|
+
` ]`,
|
|
1316
|
+
`}`,
|
|
1317
|
+
``,
|
|
1318
|
+
`Rules:`,
|
|
1319
|
+
`- Each slide in the output MUST match the type listed in the slide plan above.`,
|
|
1320
|
+
`- bullets array: max 5 items, each \u2264 80 characters.`,
|
|
1321
|
+
`- speakerNotes: 1-3 sentences a presenter would say.`,
|
|
1322
|
+
`- Derive all content from the codebase context above \u2014 no invented facts.`,
|
|
1323
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1324
|
+
].join("\n");
|
|
1325
|
+
}
|
|
1326
|
+
function buildFallback(blueprint, opts, slideTypes) {
|
|
1327
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
1328
|
+
const totalLoc = blueprint.files.reduce(
|
|
1329
|
+
(sum, f) => sum + (f.lineCount ?? 0),
|
|
1330
|
+
0
|
|
1331
|
+
);
|
|
1332
|
+
const slideBuilders = {
|
|
1333
|
+
title: () => ({
|
|
1334
|
+
type: "title",
|
|
1335
|
+
title: projectName,
|
|
1336
|
+
subtitle: "Architecture & Code Intelligence Report",
|
|
1337
|
+
speakerNotes: `Welcome. Today we cover the architecture of ${projectName}.`
|
|
1338
|
+
}),
|
|
1339
|
+
executive: () => ({
|
|
1340
|
+
type: "executive",
|
|
1341
|
+
title: "Executive Summary",
|
|
1342
|
+
bullets: [
|
|
1343
|
+
`${blueprint.stats.totalFiles} files analyzed across all layers`,
|
|
1344
|
+
`${blueprint.stats.runtimeEdges} dependency edges mapped`,
|
|
1345
|
+
`Architecture categories: app, components, lib, hooks`
|
|
1346
|
+
],
|
|
1347
|
+
speakerNotes: "Three key findings from the codebase scan."
|
|
1348
|
+
}),
|
|
1349
|
+
architecture: () => ({
|
|
1350
|
+
type: "architecture",
|
|
1351
|
+
title: "Architecture Overview",
|
|
1352
|
+
subtitle: `${blueprint.stats.totalFiles} files, ${blueprint.stats.runtimeEdges} edges`,
|
|
1353
|
+
bullets: [
|
|
1354
|
+
`App layer: ${blueprint.categories.app ?? 0} files`,
|
|
1355
|
+
`Components: ${blueprint.categories.component ?? 0} files`,
|
|
1356
|
+
`Libraries: ${blueprint.categories.lib ?? 0} files`,
|
|
1357
|
+
`Hooks: ${blueprint.categories.hook ?? 0} files`
|
|
1358
|
+
],
|
|
1359
|
+
visualHint: "Layered architecture diagram with dependency arrows",
|
|
1360
|
+
speakerNotes: "The codebase is organized into four primary layers."
|
|
1361
|
+
}),
|
|
1362
|
+
capabilities: () => ({
|
|
1363
|
+
type: "capabilities",
|
|
1364
|
+
title: "Capability Map",
|
|
1365
|
+
bullets: ["Core application logic", "Component layer", "Shared utilities", "Custom hooks"],
|
|
1366
|
+
speakerNotes: "These are the key capability areas identified in the scan."
|
|
1367
|
+
}),
|
|
1368
|
+
health: () => ({
|
|
1369
|
+
type: "health",
|
|
1370
|
+
title: "Codebase Health",
|
|
1371
|
+
highlight: "B",
|
|
1372
|
+
highlightLabel: "Overall Grade",
|
|
1373
|
+
bullets: [
|
|
1374
|
+
`${blueprint.stats.totalFiles} total files`,
|
|
1375
|
+
`${blueprint.stats.runtimeEdges} dependency edges`,
|
|
1376
|
+
`Avg LOC per file: ${blueprint.stats.totalFiles > 0 ? Math.round(totalLoc / blueprint.stats.totalFiles) : 0}`
|
|
1377
|
+
],
|
|
1378
|
+
speakerNotes: "The codebase shows good structural health with opportunities for improvement."
|
|
1379
|
+
}),
|
|
1380
|
+
risks: () => ({
|
|
1381
|
+
type: "risks",
|
|
1382
|
+
title: "Top Risks",
|
|
1383
|
+
bullets: [
|
|
1384
|
+
"High impact: Large files with many incoming dependencies",
|
|
1385
|
+
"Medium impact: Missing test coverage in critical paths",
|
|
1386
|
+
"Low impact: Inconsistent category classification"
|
|
1387
|
+
],
|
|
1388
|
+
speakerNotes: "Three risks identified, prioritized by impact."
|
|
1389
|
+
}),
|
|
1390
|
+
recommendations: () => ({
|
|
1391
|
+
type: "recommendations",
|
|
1392
|
+
title: "Recommendations",
|
|
1393
|
+
bullets: [
|
|
1394
|
+
"Break down files with high importedByCount into smaller modules",
|
|
1395
|
+
"Establish clear boundaries between app, lib, and component layers",
|
|
1396
|
+
"Add test coverage to files with the most incoming dependencies",
|
|
1397
|
+
"Review and resolve circular dependency patterns",
|
|
1398
|
+
"Document architecture decisions for new team members"
|
|
1399
|
+
],
|
|
1400
|
+
speakerNotes: "Five concrete action items, ordered by priority."
|
|
1401
|
+
}),
|
|
1402
|
+
metrics: () => ({
|
|
1403
|
+
type: "metrics",
|
|
1404
|
+
title: "Key Metrics",
|
|
1405
|
+
highlight: String(blueprint.stats.totalFiles),
|
|
1406
|
+
highlightLabel: "Total Files",
|
|
1407
|
+
bullets: [
|
|
1408
|
+
`Lines of code: ${totalLoc.toLocaleString()}`,
|
|
1409
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1410
|
+
`App files: ${blueprint.categories.app ?? 0}`,
|
|
1411
|
+
`Components: ${blueprint.categories.component ?? 0}`,
|
|
1412
|
+
`Libraries: ${blueprint.categories.lib ?? 0}`
|
|
1413
|
+
],
|
|
1414
|
+
speakerNotes: "Key quantitative metrics from the Blueprint scan."
|
|
1415
|
+
}),
|
|
1416
|
+
closing: () => ({
|
|
1417
|
+
type: "closing",
|
|
1418
|
+
title: "Next Steps",
|
|
1419
|
+
subtitle: "Start with the highest-impact recommendations",
|
|
1420
|
+
bullets: [
|
|
1421
|
+
"Schedule architecture review with the team",
|
|
1422
|
+
"Prioritize the top 3 recommendations",
|
|
1423
|
+
"Set up regular prism0x2A scans"
|
|
1424
|
+
],
|
|
1425
|
+
speakerNotes: "Thank you. Questions?"
|
|
1426
|
+
})
|
|
1427
|
+
};
|
|
1428
|
+
const slides = slideTypes.map((t) => slideBuilders[t]());
|
|
1429
|
+
return {
|
|
1430
|
+
title: `${projectName} \u2014 Architecture Deck`,
|
|
1431
|
+
subtitle: `${opts.audience.charAt(0).toUpperCase() + opts.audience.slice(1)} presentation \xB7 ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`,
|
|
1432
|
+
slideCount: slides.length,
|
|
1433
|
+
slides,
|
|
1434
|
+
theme: opts.theme,
|
|
1435
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function parseLlmResponse(raw, slideTypes, opts, blueprint) {
|
|
1439
|
+
let text = raw.trim();
|
|
1440
|
+
if (text.startsWith("```")) {
|
|
1441
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1442
|
+
}
|
|
1443
|
+
let parsed;
|
|
1444
|
+
try {
|
|
1445
|
+
parsed = JSON.parse(text);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return null;
|
|
1448
|
+
}
|
|
1449
|
+
const rawSlides = Array.isArray(parsed.slides) ? parsed.slides : [];
|
|
1450
|
+
if (rawSlides.length === 0) return null;
|
|
1451
|
+
const slides = rawSlides.map((s, i) => {
|
|
1452
|
+
const slide = s ?? {};
|
|
1453
|
+
const inferredType = typeof slide.type === "string" && [
|
|
1454
|
+
"title",
|
|
1455
|
+
"executive",
|
|
1456
|
+
"architecture",
|
|
1457
|
+
"capabilities",
|
|
1458
|
+
"health",
|
|
1459
|
+
"risks",
|
|
1460
|
+
"recommendations",
|
|
1461
|
+
"metrics",
|
|
1462
|
+
"closing"
|
|
1463
|
+
].includes(slide.type) ? slide.type : slideTypes[i] ?? "executive";
|
|
1464
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets.filter((b) => typeof b === "string").slice(0, 5) : void 0;
|
|
1465
|
+
return {
|
|
1466
|
+
type: inferredType,
|
|
1467
|
+
title: typeof slide.title === "string" ? slide.title : `Slide ${i + 1}`,
|
|
1468
|
+
...typeof slide.subtitle === "string" && slide.subtitle ? { subtitle: slide.subtitle } : {},
|
|
1469
|
+
...bullets && bullets.length > 0 ? { bullets } : {},
|
|
1470
|
+
...typeof slide.highlight === "string" && slide.highlight ? { highlight: slide.highlight } : {},
|
|
1471
|
+
...typeof slide.highlightLabel === "string" && slide.highlightLabel ? { highlightLabel: slide.highlightLabel } : {},
|
|
1472
|
+
...typeof slide.speakerNotes === "string" && slide.speakerNotes ? { speakerNotes: slide.speakerNotes } : {},
|
|
1473
|
+
...typeof slide.visualHint === "string" && slide.visualHint ? { visualHint: slide.visualHint } : {}
|
|
1474
|
+
};
|
|
1475
|
+
});
|
|
1476
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
1477
|
+
return {
|
|
1478
|
+
title: typeof parsed.title === "string" && parsed.title ? parsed.title : `${projectName} \u2014 Architecture Deck`,
|
|
1479
|
+
subtitle: typeof parsed.subtitle === "string" && parsed.subtitle ? parsed.subtitle : `${opts.audience} presentation`,
|
|
1480
|
+
slideCount: slides.length,
|
|
1481
|
+
slides,
|
|
1482
|
+
theme: opts.theme,
|
|
1483
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
async function generatePresentation(opts) {
|
|
1487
|
+
const { blueprint, llm } = opts;
|
|
1488
|
+
const slideTypes = planSlideTypes(opts);
|
|
1489
|
+
try {
|
|
1490
|
+
const response = await llm.complete({
|
|
1491
|
+
systemPrompt: buildSystemPrompt7(opts),
|
|
1492
|
+
messages: [
|
|
1493
|
+
{
|
|
1494
|
+
role: "user",
|
|
1495
|
+
content: buildUserPrompt7(blueprint, opts, slideTypes)
|
|
1496
|
+
}
|
|
1497
|
+
],
|
|
1498
|
+
maxTokens: 4096
|
|
1499
|
+
});
|
|
1500
|
+
const parsed = parseLlmResponse(response.content, slideTypes, opts, blueprint);
|
|
1501
|
+
if (parsed) return parsed;
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
return buildFallback(blueprint, opts, slideTypes);
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/generators/generateComplianceDoc.ts
|
|
1508
|
+
var FRAMEWORK_SPECS = {
|
|
1509
|
+
SOX: {
|
|
1510
|
+
fullName: "Sarbanes-Oxley Act (SOX)",
|
|
1511
|
+
focusAreas: [
|
|
1512
|
+
"Internal controls over financial reporting (ICFR)",
|
|
1513
|
+
"Access control and segregation of duties",
|
|
1514
|
+
"Change management procedures",
|
|
1515
|
+
"Audit trail completeness",
|
|
1516
|
+
"IT general controls (ITGCs)"
|
|
1517
|
+
],
|
|
1518
|
+
requiredSections: [
|
|
1519
|
+
"Scope and System Boundaries",
|
|
1520
|
+
"IT General Controls",
|
|
1521
|
+
"Access Control and Segregation of Duties",
|
|
1522
|
+
"Change Management",
|
|
1523
|
+
"Audit Trail and Logging",
|
|
1524
|
+
"Financial Reporting Controls"
|
|
1525
|
+
],
|
|
1526
|
+
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."
|
|
1527
|
+
},
|
|
1528
|
+
ISO27001: {
|
|
1529
|
+
fullName: "ISO/IEC 27001:2022 Information Security Management",
|
|
1530
|
+
focusAreas: [
|
|
1531
|
+
"Information security policies (Clause 5)",
|
|
1532
|
+
"Risk assessment and treatment (Clause 6)",
|
|
1533
|
+
"Information assets classification",
|
|
1534
|
+
"Access control (Annex A 5.15\u20135.18)",
|
|
1535
|
+
"Cryptography (Annex A 8.24)",
|
|
1536
|
+
"Supplier relationships (Annex A 5.19\u20135.22)"
|
|
1537
|
+
],
|
|
1538
|
+
requiredSections: [
|
|
1539
|
+
"Scope and Context",
|
|
1540
|
+
"Information Assets",
|
|
1541
|
+
"Risk Assessment",
|
|
1542
|
+
"Access Control",
|
|
1543
|
+
"Cryptography and Data Protection",
|
|
1544
|
+
"Incident Management",
|
|
1545
|
+
"Business Continuity"
|
|
1546
|
+
],
|
|
1547
|
+
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."
|
|
1548
|
+
},
|
|
1549
|
+
GDPR: {
|
|
1550
|
+
fullName: "General Data Protection Regulation (GDPR)",
|
|
1551
|
+
focusAreas: [
|
|
1552
|
+
"Article 30 Records of Processing Activities (ROPA)",
|
|
1553
|
+
"Lawful basis for processing",
|
|
1554
|
+
"Data subject rights (Articles 15\u201322)",
|
|
1555
|
+
"Data retention and deletion",
|
|
1556
|
+
"Data transfers (Chapter V)",
|
|
1557
|
+
"Privacy by design and default (Article 25)"
|
|
1558
|
+
],
|
|
1559
|
+
requiredSections: [
|
|
1560
|
+
"Records of Processing Activities (ROPA)",
|
|
1561
|
+
"Lawful Basis for Processing",
|
|
1562
|
+
"Data Subject Rights",
|
|
1563
|
+
"Data Retention Policy",
|
|
1564
|
+
"International Data Transfers",
|
|
1565
|
+
"Privacy by Design",
|
|
1566
|
+
"Data Breach Response"
|
|
1567
|
+
],
|
|
1568
|
+
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."
|
|
1569
|
+
},
|
|
1570
|
+
SOC2: {
|
|
1571
|
+
fullName: "SOC 2 Type II (Trust Services Criteria)",
|
|
1572
|
+
focusAreas: [
|
|
1573
|
+
"Security (Common Criteria \u2014 CC)",
|
|
1574
|
+
"Availability (Availability Criteria \u2014 A)",
|
|
1575
|
+
"Confidentiality (Confidentiality Criteria \u2014 C)",
|
|
1576
|
+
"Processing Integrity (PI)",
|
|
1577
|
+
"Privacy (P)"
|
|
1578
|
+
],
|
|
1579
|
+
requiredSections: [
|
|
1580
|
+
"System Description",
|
|
1581
|
+
"Security \u2014 Common Criteria",
|
|
1582
|
+
"Availability",
|
|
1583
|
+
"Confidentiality",
|
|
1584
|
+
"Change Management",
|
|
1585
|
+
"Monitoring",
|
|
1586
|
+
"Vendor Management"
|
|
1587
|
+
],
|
|
1588
|
+
sectionGuidance: "Follow the AICPA Trust Services Criteria. For each section, identify relevant capabilities, current controls, and any gaps versus the criteria."
|
|
1589
|
+
},
|
|
1590
|
+
CUSTOM: {
|
|
1591
|
+
fullName: "Custom Compliance Framework",
|
|
1592
|
+
focusAreas: [
|
|
1593
|
+
"Scope and applicability",
|
|
1594
|
+
"Control requirements",
|
|
1595
|
+
"Risk assessment",
|
|
1596
|
+
"Monitoring and audit"
|
|
1597
|
+
],
|
|
1598
|
+
requiredSections: [
|
|
1599
|
+
"Scope",
|
|
1600
|
+
"Controls Inventory",
|
|
1601
|
+
"Risk Assessment",
|
|
1602
|
+
"Gaps and Recommendations"
|
|
1603
|
+
],
|
|
1604
|
+
sectionGuidance: "Generate a structured compliance document based on the codebase capabilities and general best practices."
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
function buildCodebaseContext(blueprint, opts) {
|
|
1608
|
+
const lines = [
|
|
1609
|
+
`## Codebase Overview`,
|
|
1610
|
+
`Project: ${opts.projectName}`,
|
|
1611
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
1612
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`
|
|
1613
|
+
];
|
|
1614
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
|
|
1615
|
+
lines.push(``, `## Most-imported files (likely critical components)`);
|
|
1616
|
+
for (const f of topFiles) {
|
|
1617
|
+
lines.push(`- ${f.path} [${f.category ?? "unknown"}] \u2014 imported by ${f.importedByCount ?? 0} modules`);
|
|
1618
|
+
}
|
|
1619
|
+
return lines.join("\n");
|
|
1620
|
+
}
|
|
1621
|
+
function buildAmberContext(amber) {
|
|
1622
|
+
const lines = [`## AMBER Capability Registry`, amber.summary, ``];
|
|
1623
|
+
for (const cap of amber.capabilities.slice(0, 20)) {
|
|
1624
|
+
const tags = [cap.criticality, cap.lifecycle];
|
|
1625
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
1626
|
+
if (cap.hasTests) tags.push("documented");
|
|
1627
|
+
lines.push(
|
|
1628
|
+
`- ${cap.name} (${cap.id}) [${tags.join(", ")}]` + (cap.description ? `: ${cap.description}` : "")
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
return lines.join("\n");
|
|
1632
|
+
}
|
|
1633
|
+
function buildComplianceContext(ctx, framework) {
|
|
1634
|
+
const lines = [
|
|
1635
|
+
`## PRISM Compliance Analysis`,
|
|
1636
|
+
`Framework: ${ctx.framework}`,
|
|
1637
|
+
`Total capabilities: ${ctx.totalCapabilities}`,
|
|
1638
|
+
`Risk summary \u2014 High: ${ctx.riskSummary.high}, Medium: ${ctx.riskSummary.medium}, Low: ${ctx.riskSummary.low}`,
|
|
1639
|
+
``,
|
|
1640
|
+
`### Critical / High Capabilities`
|
|
1641
|
+
];
|
|
1642
|
+
for (const cap of ctx.criticalCapabilities.slice(0, 15)) {
|
|
1643
|
+
const tags = [cap.criticality];
|
|
1644
|
+
if (cap.accessControl) tags.push("access-control");
|
|
1645
|
+
if (cap.auditTrail) tags.push("audit-trail");
|
|
1646
|
+
if (cap.dataProcessing) tags.push("data-processing");
|
|
1647
|
+
if (!cap.testCoverage) tags.push("no-doc");
|
|
1648
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
1649
|
+
lines.push(`- ${cap.name} (${cap.id}) [${tags.join(", ")}]`);
|
|
1650
|
+
}
|
|
1651
|
+
if (framework === "SOX" && ctx.sox) {
|
|
1652
|
+
lines.push(``, `### SOX: Financial capabilities: ${ctx.sox.financialCapabilities.join(", ") || "none identified"}`);
|
|
1653
|
+
lines.push(`SOX: Access-controlled: ${ctx.sox.accessControlled.join(", ") || "none identified"}`);
|
|
1654
|
+
}
|
|
1655
|
+
if (framework === "GDPR" && ctx.gdpr) {
|
|
1656
|
+
lines.push(``, `### GDPR: Data processing capabilities: ${ctx.gdpr.dataProcessingCapabilities.join(", ") || "none identified"}`);
|
|
1657
|
+
lines.push(`GDPR: Retention capabilities: ${ctx.gdpr.retentionCapabilities.join(", ") || "none identified"}`);
|
|
1658
|
+
}
|
|
1659
|
+
if (framework === "ISO27001" && ctx.iso27001) {
|
|
1660
|
+
lines.push(``, `### ISO27001: Information assets: ${ctx.iso27001.informationAssets.join(", ") || "none identified"}`);
|
|
1661
|
+
lines.push(`ISO27001: Security controls: ${ctx.iso27001.securityControls.join(", ") || "none identified"}`);
|
|
1662
|
+
}
|
|
1663
|
+
if (framework === "SOC2" && ctx.soc2) {
|
|
1664
|
+
lines.push(``, `### SOC2: Security capabilities: ${ctx.soc2.securityCapabilities.join(", ") || "none identified"}`);
|
|
1665
|
+
lines.push(`SOC2: Availability capabilities: ${ctx.soc2.availabilityCapabilities.join(", ") || "none identified"}`);
|
|
1666
|
+
}
|
|
1667
|
+
return lines.join("\n");
|
|
1668
|
+
}
|
|
1669
|
+
function buildSystemPrompt8(opts) {
|
|
1670
|
+
const toneMap = {
|
|
1671
|
+
formal: "You are a senior compliance officer writing a formal regulatory compliance document for external auditors and regulators.",
|
|
1672
|
+
technical: "You are a security-focused software architect writing a technical compliance assessment grounded in code-level evidence.",
|
|
1673
|
+
executive: "You are a Chief Compliance Officer writing a concise executive summary compliance report for board-level review."
|
|
1674
|
+
};
|
|
1675
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
1676
|
+
}
|
|
1677
|
+
function buildUserPrompt8(opts) {
|
|
1678
|
+
const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
|
|
1679
|
+
const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
|
|
1680
|
+
const contextParts = [
|
|
1681
|
+
buildCodebaseContext(opts.blueprint, opts)
|
|
1682
|
+
];
|
|
1683
|
+
if (opts.amberContext) contextParts.push(buildAmberContext(opts.amberContext));
|
|
1684
|
+
if (opts.complianceContext) contextParts.push(buildComplianceContext(opts.complianceContext, opts.framework));
|
|
1685
|
+
const org = opts.organizationName ? `Organization: ${opts.organizationName}` : "";
|
|
1686
|
+
const recsInstruction = opts.includeRecommendations ? 'Include a "recommendations" array with 3\u20137 actionable remediation items.' : 'Set "recommendations" to an empty array.';
|
|
1687
|
+
return [
|
|
1688
|
+
`Generate a ${frameworkName} compliance document for: ${opts.projectName}`,
|
|
1689
|
+
org,
|
|
1690
|
+
`Tone: ${opts.tone}`,
|
|
1691
|
+
``,
|
|
1692
|
+
contextParts.join("\n\n"),
|
|
1693
|
+
``,
|
|
1694
|
+
`## Framework: ${frameworkName}`,
|
|
1695
|
+
`Focus areas:`,
|
|
1696
|
+
spec.focusAreas.map((f) => `- ${f}`).join("\n"),
|
|
1697
|
+
``,
|
|
1698
|
+
spec.sectionGuidance,
|
|
1699
|
+
``,
|
|
1700
|
+
`Required sections: ${spec.requiredSections.join(", ")}`,
|
|
1701
|
+
``,
|
|
1702
|
+
`## Output Format`,
|
|
1703
|
+
`Return a single JSON object with this exact shape:`,
|
|
1704
|
+
`{`,
|
|
1705
|
+
` "documentTitle": "<formal document title>",`,
|
|
1706
|
+
` "executiveSummary": "<3\u20135 sentences summarising compliance posture>",`,
|
|
1707
|
+
` "confidentiality": "INTERNAL" | "CONFIDENTIAL" | "PUBLIC",`,
|
|
1708
|
+
` "complianceGaps": ["<gap 1>", "<gap 2>"],`,
|
|
1709
|
+
` "recommendations": ["<rec 1>", "<rec 2>"],`,
|
|
1710
|
+
` "sections": [`,
|
|
1711
|
+
` {`,
|
|
1712
|
+
` "title": "<section title>",`,
|
|
1713
|
+
` "content": "<substantive section prose, 2\u20134 paragraphs>",`,
|
|
1714
|
+
` "capabilities": ["<capability id 1>"],`,
|
|
1715
|
+
` "riskLevel": "high" | "medium" | "low",`,
|
|
1716
|
+
` "status": "compliant" | "partial" | "gap"`,
|
|
1717
|
+
` }`,
|
|
1718
|
+
` ]`,
|
|
1719
|
+
`}`,
|
|
1720
|
+
``,
|
|
1721
|
+
`Rules:`,
|
|
1722
|
+
`- Derive ALL content from the codebase context above \u2014 no invented facts.`,
|
|
1723
|
+
`- Each section MUST reference real capability IDs from the AMBER registry where available.`,
|
|
1724
|
+
`- complianceGaps: list specific gaps found (missing controls, undocumented capabilities, drift).`,
|
|
1725
|
+
recsInstruction,
|
|
1726
|
+
`- confidentiality: CONFIDENTIAL for SOX/ISO27001/SOC2, INTERNAL for GDPR/CUSTOM.`,
|
|
1727
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1728
|
+
].filter(Boolean).join("\n");
|
|
1729
|
+
}
|
|
1730
|
+
function buildFallback2(opts) {
|
|
1731
|
+
const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
|
|
1732
|
+
const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
|
|
1733
|
+
const totalCaps = opts.complianceContext?.totalCapabilities ?? opts.amberContext?.capabilities.length ?? 0;
|
|
1734
|
+
const highRisk = opts.complianceContext?.riskSummary.high ?? 0;
|
|
1735
|
+
const sections = spec.requiredSections.map((title) => ({
|
|
1736
|
+
title,
|
|
1737
|
+
content: `Assessment of ${title.toLowerCase()} for ${opts.projectName}. Based on Blueprint analysis, ${totalCaps} capabilities were identified for review under ${frameworkName}.`,
|
|
1738
|
+
capabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
|
|
1739
|
+
riskLevel: "medium",
|
|
1740
|
+
status: "partial"
|
|
1741
|
+
}));
|
|
1742
|
+
return {
|
|
1743
|
+
framework: opts.framework,
|
|
1744
|
+
documentTitle: `${opts.projectName} \u2014 ${frameworkName} Compliance Assessment`,
|
|
1745
|
+
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.`,
|
|
1746
|
+
sections,
|
|
1747
|
+
complianceGaps: [
|
|
1748
|
+
`${totalCaps - (opts.amberContext?.taggedFiles ?? 0)} files lack capability annotations \u2014 compliance traceability is incomplete.`,
|
|
1749
|
+
`${opts.amberContext?.driftedCapabilities ?? 0} capabilities have documentation drift \u2014 controls may be misrepresented.`
|
|
1750
|
+
],
|
|
1751
|
+
recommendations: opts.includeRecommendations ? [
|
|
1752
|
+
"Ensure all critical capabilities have up-to-date @amber-doc annotations.",
|
|
1753
|
+
"Implement automated compliance scanning in CI/CD pipeline.",
|
|
1754
|
+
"Conduct quarterly capability review against framework requirements."
|
|
1755
|
+
] : [],
|
|
1756
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1757
|
+
confidentiality: opts.framework === "GDPR" || opts.framework === "CUSTOM" ? "INTERNAL" : "CONFIDENTIAL"
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function parseLlmResponse2(raw, opts) {
|
|
1761
|
+
let text = raw.trim();
|
|
1762
|
+
if (text.startsWith("```")) {
|
|
1763
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1764
|
+
}
|
|
1765
|
+
let parsed;
|
|
1766
|
+
try {
|
|
1767
|
+
parsed = JSON.parse(text);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return null;
|
|
1770
|
+
}
|
|
1771
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
1772
|
+
if (rawSections.length === 0) return null;
|
|
1773
|
+
const sections = rawSections.map((s) => {
|
|
1774
|
+
const sec = s ?? {};
|
|
1775
|
+
return {
|
|
1776
|
+
title: typeof sec.title === "string" ? sec.title : "Section",
|
|
1777
|
+
content: typeof sec.content === "string" ? sec.content : "",
|
|
1778
|
+
capabilities: Array.isArray(sec.capabilities) ? sec.capabilities.filter((c) => typeof c === "string") : [],
|
|
1779
|
+
...typeof sec.riskLevel === "string" && ["high", "medium", "low"].includes(sec.riskLevel) ? { riskLevel: sec.riskLevel } : {},
|
|
1780
|
+
...typeof sec.status === "string" && ["compliant", "partial", "gap"].includes(sec.status) ? { status: sec.status } : {}
|
|
1781
|
+
};
|
|
1782
|
+
});
|
|
1783
|
+
const confidentiality = parsed.confidentiality === "CONFIDENTIAL" ? "CONFIDENTIAL" : parsed.confidentiality === "PUBLIC" ? "PUBLIC" : "INTERNAL";
|
|
1784
|
+
return {
|
|
1785
|
+
framework: opts.framework,
|
|
1786
|
+
documentTitle: typeof parsed.documentTitle === "string" ? parsed.documentTitle : `${opts.projectName} \u2014 Compliance Assessment`,
|
|
1787
|
+
sections,
|
|
1788
|
+
executiveSummary: typeof parsed.executiveSummary === "string" ? parsed.executiveSummary : "",
|
|
1789
|
+
complianceGaps: Array.isArray(parsed.complianceGaps) ? parsed.complianceGaps.filter((g) => typeof g === "string") : [],
|
|
1790
|
+
recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations.filter((r) => typeof r === "string") : [],
|
|
1791
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1792
|
+
confidentiality
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
async function generateComplianceDoc(opts) {
|
|
1796
|
+
const systemPrompt = buildSystemPrompt8(opts);
|
|
1797
|
+
const userPrompt = buildUserPrompt8(opts);
|
|
1798
|
+
try {
|
|
1799
|
+
const response = await opts.llm.complete({
|
|
1800
|
+
systemPrompt,
|
|
1801
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
1802
|
+
maxTokens: 6e3
|
|
1803
|
+
});
|
|
1804
|
+
const parsed = parseLlmResponse2(response.content, opts);
|
|
1805
|
+
if (parsed) return parsed;
|
|
1806
|
+
} catch {
|
|
1807
|
+
}
|
|
1808
|
+
return buildFallback2(opts);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/generators/generateADR.ts
|
|
1812
|
+
function buildCodebaseContext2(blueprint, projectName) {
|
|
1813
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
|
|
1814
|
+
const totalLoc = blueprint.files.reduce((sum, f) => sum + (f.lineCount ?? 0), 0);
|
|
1815
|
+
const lines = [
|
|
1816
|
+
`## Codebase Overview`,
|
|
1817
|
+
`Project: ${projectName}`,
|
|
1818
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
1819
|
+
`Total LOC: ${totalLoc.toLocaleString()}`,
|
|
1820
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1821
|
+
``,
|
|
1822
|
+
`## Most-imported files (architectural hotspots)`
|
|
1823
|
+
];
|
|
1824
|
+
for (const f of topFiles) {
|
|
1825
|
+
lines.push(`- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} modules, ${f.lineCount ?? 0} lines`);
|
|
1826
|
+
}
|
|
1827
|
+
if (blueprint.edges.length > 0) {
|
|
1828
|
+
const edgeSample = blueprint.edges.slice(0, 8);
|
|
1829
|
+
lines.push(``, `## Dependency sample`);
|
|
1830
|
+
for (const e of edgeSample) {
|
|
1831
|
+
lines.push(`- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
return lines.join("\n");
|
|
1835
|
+
}
|
|
1836
|
+
function buildAdrContext(ctx, focus, selectedCycle, selectedCapability) {
|
|
1837
|
+
const lines = [`## PRISM Analysis`];
|
|
1838
|
+
if (ctx.coherenceScore !== null) {
|
|
1839
|
+
lines.push(`Architecture Coherence Score: ${ctx.coherenceScore}/100`);
|
|
1840
|
+
}
|
|
1841
|
+
if (ctx.architecturalPatterns.length > 0) {
|
|
1842
|
+
lines.push(`Detected patterns: ${ctx.architecturalPatterns.join(", ")}`);
|
|
1843
|
+
}
|
|
1844
|
+
if (ctx.topRisks.length > 0) {
|
|
1845
|
+
lines.push(``, `### Top Architectural Risks`);
|
|
1846
|
+
for (const r of ctx.topRisks.slice(0, 5)) {
|
|
1847
|
+
lines.push(`- ${r.name} (${r.capabilityId}): ${r.reason}`);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if ((focus === "cycles" || focus === "overall") && ctx.importCycles.length > 0) {
|
|
1851
|
+
lines.push(``, `### Import Cycles (${ctx.importCycles.length} detected)`);
|
|
1852
|
+
const cyclesToShow = selectedCycle ? ctx.importCycles.filter((c) => c.files.some((f) => selectedCycle.includes(f))) : ctx.importCycles.slice(0, 5);
|
|
1853
|
+
for (const c of cyclesToShow) {
|
|
1854
|
+
lines.push(`- [${c.severity}] ${c.files.join(" \u2192 ")}`);
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
if ((focus === "capabilities" || focus === "overall") && ctx.capabilityBoundaries.length > 0) {
|
|
1858
|
+
lines.push(``, `### Capability Boundaries (dependency edges)`);
|
|
1859
|
+
const boundaries = selectedCapability ? ctx.capabilityBoundaries.filter((b) => b.from === selectedCapability || b.to === selectedCapability) : ctx.capabilityBoundaries.slice(0, 10);
|
|
1860
|
+
for (const b of boundaries) {
|
|
1861
|
+
lines.push(`- ${b.from} \u2192 ${b.to} (${b.edgeCount} import edges)`);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if ((focus === "dependencies" || focus === "overall") && ctx.highChurnFiles.length > 0) {
|
|
1865
|
+
lines.push(``, `### High-Churn Files`);
|
|
1866
|
+
for (const f of ctx.highChurnFiles.slice(0, 8)) {
|
|
1867
|
+
const caps = f.capabilities.length > 0 ? ` [${f.capabilities.join(", ")}]` : "";
|
|
1868
|
+
lines.push(`- ${f.path}${caps} \u2014 ${f.commits} commits`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (ctx.orphanedFiles.length > 0) {
|
|
1872
|
+
lines.push(``, `### Orphaned Files (${ctx.orphanedFiles.length} untagged)`);
|
|
1873
|
+
for (const f of ctx.orphanedFiles.slice(0, 5)) {
|
|
1874
|
+
lines.push(`- ${f}`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
return lines.join("\n");
|
|
1878
|
+
}
|
|
1879
|
+
function buildAmberContext2(amber) {
|
|
1880
|
+
const lines = [`## AMBER Capability Registry`, amber.summary, ``];
|
|
1881
|
+
for (const cap of amber.capabilities.slice(0, 15)) {
|
|
1882
|
+
const tags = [cap.criticality, cap.lifecycle];
|
|
1883
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
1884
|
+
lines.push(
|
|
1885
|
+
`- ${cap.name} (${cap.id}) [${tags.join(", ")}]${cap.files.length > 0 ? ` \u2014 ${cap.files.length} files` : ""}`
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
return lines.join("\n");
|
|
1889
|
+
}
|
|
1890
|
+
function buildSystemPrompt9() {
|
|
1891
|
+
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.";
|
|
1892
|
+
}
|
|
1893
|
+
function buildUserPrompt9(opts) {
|
|
1894
|
+
const focusDescriptions = {
|
|
1895
|
+
overall: "Generate a comprehensive set of ADRs covering the most significant architectural decisions evident from import cycles, capability boundaries, dependency patterns, and code structure.",
|
|
1896
|
+
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.",
|
|
1897
|
+
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.",
|
|
1898
|
+
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."
|
|
1899
|
+
};
|
|
1900
|
+
const parts = [
|
|
1901
|
+
`Generate 3\u20137 Architecture Decision Records (ADRs) for: ${opts.projectName}`,
|
|
1902
|
+
`Focus: ${opts.focus} \u2014 ${focusDescriptions[opts.focus]}`,
|
|
1903
|
+
``,
|
|
1904
|
+
buildCodebaseContext2(opts.blueprint, opts.projectName)
|
|
1905
|
+
];
|
|
1906
|
+
if (opts.amberContext) {
|
|
1907
|
+
parts.push(``, buildAmberContext2(opts.amberContext));
|
|
1908
|
+
}
|
|
1909
|
+
if (opts.adrContext) {
|
|
1910
|
+
parts.push(``, buildAdrContext(opts.adrContext, opts.focus, opts.selectedCycle, opts.selectedCapability));
|
|
1911
|
+
}
|
|
1912
|
+
if (opts.selectedCycle && opts.selectedCycle.length > 0) {
|
|
1913
|
+
parts.push(``, `## Selected Cycle for Focus`, opts.selectedCycle.join(" \u2192 "));
|
|
1914
|
+
}
|
|
1915
|
+
if (opts.selectedCapability) {
|
|
1916
|
+
parts.push(``, `## Selected Capability for Focus: ${opts.selectedCapability}`);
|
|
1917
|
+
}
|
|
1918
|
+
parts.push(
|
|
1919
|
+
``,
|
|
1920
|
+
`## Output Format`,
|
|
1921
|
+
`Return a single JSON object:`,
|
|
1922
|
+
`{`,
|
|
1923
|
+
` "summary": "<2\u20133 sentence overall architectural assessment>",`,
|
|
1924
|
+
` "technicalDebt": "<1\u20132 sentence estimate of technical debt severity>",`,
|
|
1925
|
+
` "adrs": [`,
|
|
1926
|
+
` {`,
|
|
1927
|
+
` "id": "ADR-001",`,
|
|
1928
|
+
` "title": "<short imperative title>",`,
|
|
1929
|
+
` "date": "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}",`,
|
|
1930
|
+
` "status": "Proposed",`,
|
|
1931
|
+
` "context": "<what is the situation and why does this decision need to be made? 2\u20134 sentences>",`,
|
|
1932
|
+
` "decision": "<what was decided and why? 2\u20134 sentences with rationale>",`,
|
|
1933
|
+
` "consequences": {`,
|
|
1934
|
+
` "positive": ["<benefit 1>", "<benefit 2>"],`,
|
|
1935
|
+
` "negative": ["<drawback 1>"],`,
|
|
1936
|
+
` "neutral": ["<neutral fact>"]`,
|
|
1937
|
+
` },`,
|
|
1938
|
+
` "alternatives": ["<alternative option 1>", "<alternative option 2>"],`,
|
|
1939
|
+
` "relatedCapabilities": ["<capability-id-1>"],`,
|
|
1940
|
+
` "relatedFiles": ["<file/path.ts>"]`,
|
|
1941
|
+
` }`,
|
|
1942
|
+
` ]`,
|
|
1943
|
+
`}`,
|
|
1944
|
+
``,
|
|
1945
|
+
`Rules:`,
|
|
1946
|
+
`- IDs: ADR-001, ADR-002, \u2026 in order.`,
|
|
1947
|
+
`- Derive ALL content from the codebase data above \u2014 no invented facts.`,
|
|
1948
|
+
`- Each ADR MUST address a real pattern visible in the data (cycle, boundary, dependency).`,
|
|
1949
|
+
`- relatedCapabilities: reference real capability IDs from AMBER registry when available.`,
|
|
1950
|
+
`- relatedFiles: reference real file paths from the blueprint.`,
|
|
1951
|
+
`- status is always "Proposed" for new ADRs.`,
|
|
1952
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1953
|
+
);
|
|
1954
|
+
return parts.join("\n");
|
|
1955
|
+
}
|
|
1956
|
+
function buildFallback3(opts) {
|
|
1957
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1958
|
+
const adrs = [];
|
|
1959
|
+
if (opts.focus === "cycles" || opts.focus === "overall") {
|
|
1960
|
+
const cycleFiles = opts.adrContext?.importCycles[0]?.files ?? opts.selectedCycle ?? [];
|
|
1961
|
+
adrs.push({
|
|
1962
|
+
id: "ADR-001",
|
|
1963
|
+
title: "Introduce Module Boundary Abstractions to Break Import Cycles",
|
|
1964
|
+
date: today,
|
|
1965
|
+
status: "Proposed",
|
|
1966
|
+
context: `${opts.projectName} contains ${opts.adrContext?.importCycles.length ?? "multiple"} detected import cycles. Circular dependencies create tight coupling that makes independent testing and deployment difficult.`,
|
|
1967
|
+
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.",
|
|
1968
|
+
consequences: {
|
|
1969
|
+
positive: ["Enables independent testing of cyclic modules", "Reduces coupling between layers"],
|
|
1970
|
+
negative: ["Requires refactoring existing imports", "Short-term increase in file count"],
|
|
1971
|
+
neutral: ["Aligns with Dependency Inversion Principle"]
|
|
1972
|
+
},
|
|
1973
|
+
alternatives: ["Extract shared state to a separate module", "Use event-driven communication between modules"],
|
|
1974
|
+
relatedCapabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
|
|
1975
|
+
relatedFiles: cycleFiles.slice(0, 3)
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
if (opts.focus === "capabilities" || opts.focus === "overall") {
|
|
1979
|
+
adrs.push({
|
|
1980
|
+
id: `ADR-00${adrs.length + 1}`,
|
|
1981
|
+
title: "Enforce Explicit Capability Boundaries with Barrel Exports",
|
|
1982
|
+
date: today,
|
|
1983
|
+
status: "Proposed",
|
|
1984
|
+
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.`,
|
|
1985
|
+
decision: "Each capability should expose a single barrel index file. External modules import only from the barrel, never from internal implementation files.",
|
|
1986
|
+
consequences: {
|
|
1987
|
+
positive: ["Capability-level API surfaces become explicit", "Refactoring internals doesn't break consumers"],
|
|
1988
|
+
negative: ["Requires creating barrel files for each capability", "Barrel files need maintenance"],
|
|
1989
|
+
neutral: ["Standard pattern in large TypeScript codebases"]
|
|
1990
|
+
},
|
|
1991
|
+
alternatives: ["Use ESLint import boundaries rules", "Monorepo packages per capability"],
|
|
1992
|
+
relatedCapabilities: opts.amberContext?.capabilities.slice(0, 3).map((c) => c.id) ?? [],
|
|
1993
|
+
relatedFiles: []
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
adrs.push({
|
|
1997
|
+
id: `ADR-00${adrs.length + 1}`,
|
|
1998
|
+
title: "Establish Architectural Fitness Functions for Ongoing Compliance",
|
|
1999
|
+
date: today,
|
|
2000
|
+
status: "Proposed",
|
|
2001
|
+
context: `The ${opts.projectName} codebase currently lacks automated enforcement of architectural constraints. Without fitness functions, architectural drift accumulates silently.`,
|
|
2002
|
+
decision: "Integrate PRISM / prism0x2A scans into CI to track coherence score, import cycles, and capability drift. Gate merges on cycle count not increasing.",
|
|
2003
|
+
consequences: {
|
|
2004
|
+
positive: ["Architectural health is continuously measured", "Teams get early warning of degradation"],
|
|
2005
|
+
negative: ["CI time increases slightly", "Requires PRISM setup in CI environment"],
|
|
2006
|
+
neutral: ["Shift from reactive to proactive architecture governance"]
|
|
2007
|
+
},
|
|
2008
|
+
alternatives: ["Manual architecture review cadence", "Weekly automated reports only"],
|
|
2009
|
+
relatedCapabilities: [],
|
|
2010
|
+
relatedFiles: []
|
|
2011
|
+
});
|
|
2012
|
+
return {
|
|
2013
|
+
adrs,
|
|
2014
|
+
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.`,
|
|
2015
|
+
technicalDebt: "Moderate-to-high architectural debt evident from cycle patterns and implicit capability boundaries. Estimated 3\u20136 engineering weeks to implement the proposed decisions."
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
function parseLlmResponse3(raw) {
|
|
2019
|
+
let text = raw.trim();
|
|
2020
|
+
if (text.startsWith("```")) {
|
|
2021
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
2022
|
+
}
|
|
2023
|
+
let parsed;
|
|
2024
|
+
try {
|
|
2025
|
+
parsed = JSON.parse(text);
|
|
2026
|
+
} catch {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
const rawAdrs = Array.isArray(parsed.adrs) ? parsed.adrs : [];
|
|
2030
|
+
if (rawAdrs.length === 0) return null;
|
|
2031
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2032
|
+
const adrs = rawAdrs.map((a, idx) => {
|
|
2033
|
+
const adr = a ?? {};
|
|
2034
|
+
const consequences = typeof adr.consequences === "object" && adr.consequences !== null ? adr.consequences : {};
|
|
2035
|
+
const status = typeof adr.status === "string" && ["Proposed", "Accepted", "Deprecated", "Superseded"].includes(adr.status) ? adr.status : "Proposed";
|
|
2036
|
+
return {
|
|
2037
|
+
id: typeof adr.id === "string" ? adr.id : `ADR-${String(idx + 1).padStart(3, "0")}`,
|
|
2038
|
+
title: typeof adr.title === "string" ? adr.title : `Decision ${idx + 1}`,
|
|
2039
|
+
date: typeof adr.date === "string" ? adr.date : today,
|
|
2040
|
+
status,
|
|
2041
|
+
context: typeof adr.context === "string" ? adr.context : "",
|
|
2042
|
+
decision: typeof adr.decision === "string" ? adr.decision : "",
|
|
2043
|
+
consequences: {
|
|
2044
|
+
positive: Array.isArray(consequences.positive) ? consequences.positive.filter((s) => typeof s === "string") : [],
|
|
2045
|
+
negative: Array.isArray(consequences.negative) ? consequences.negative.filter((s) => typeof s === "string") : [],
|
|
2046
|
+
neutral: Array.isArray(consequences.neutral) ? consequences.neutral.filter((s) => typeof s === "string") : []
|
|
2047
|
+
},
|
|
2048
|
+
alternatives: Array.isArray(adr.alternatives) ? adr.alternatives.filter((s) => typeof s === "string") : [],
|
|
2049
|
+
relatedCapabilities: Array.isArray(adr.relatedCapabilities) ? adr.relatedCapabilities.filter((s) => typeof s === "string") : [],
|
|
2050
|
+
relatedFiles: Array.isArray(adr.relatedFiles) ? adr.relatedFiles.filter((s) => typeof s === "string") : []
|
|
2051
|
+
};
|
|
2052
|
+
});
|
|
2053
|
+
return {
|
|
2054
|
+
adrs,
|
|
2055
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
2056
|
+
technicalDebt: typeof parsed.technicalDebt === "string" ? parsed.technicalDebt : ""
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
async function generateADR(opts) {
|
|
2060
|
+
try {
|
|
2061
|
+
const response = await opts.llm.complete({
|
|
2062
|
+
systemPrompt: buildSystemPrompt9(),
|
|
2063
|
+
messages: [{ role: "user", content: buildUserPrompt9(opts) }],
|
|
2064
|
+
maxTokens: 6e3
|
|
2065
|
+
});
|
|
2066
|
+
const parsed = parseLlmResponse3(response.content);
|
|
2067
|
+
if (parsed) return parsed;
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
return buildFallback3(opts);
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// src/generators/generateSprintRetro.ts
|
|
2074
|
+
function classifyCommit(subject) {
|
|
2075
|
+
const s = subject.toLowerCase();
|
|
2076
|
+
if (s.startsWith("feat") || s.includes("add ") || s.includes("implement") || s.includes("new ")) return "feature";
|
|
2077
|
+
if (s.startsWith("fix") || s.includes("bug") || s.includes("patch") || s.includes("hotfix")) return "fix";
|
|
2078
|
+
if (s.startsWith("refactor") || s.startsWith("refact") || s.includes("rewrite") || s.includes("restructure")) return "refactor";
|
|
2079
|
+
return "chore";
|
|
2080
|
+
}
|
|
2081
|
+
function buildContext2(opts) {
|
|
2082
|
+
const lines = [
|
|
2083
|
+
`## Sprint Context`,
|
|
2084
|
+
`Project: ${opts.projectName}`,
|
|
2085
|
+
`Sprint: ${opts.sprintName ?? "Current Sprint"}`,
|
|
2086
|
+
`Team size: ${opts.teamSize ?? "unknown"}`,
|
|
2087
|
+
`Tone: ${opts.tone}`,
|
|
2088
|
+
``
|
|
2089
|
+
];
|
|
2090
|
+
if (opts.gitContext) {
|
|
2091
|
+
const { commits, filesChanged, fromRef, toRef } = opts.gitContext;
|
|
2092
|
+
lines.push(
|
|
2093
|
+
`## Git Activity (${fromRef} \u2192 ${toRef})`,
|
|
2094
|
+
`Commits: ${commits.length}`,
|
|
2095
|
+
`Files changed: ${filesChanged}`,
|
|
2096
|
+
``,
|
|
2097
|
+
`### Commits`,
|
|
2098
|
+
...commits.slice(0, 30).map(
|
|
2099
|
+
(c) => `- [${classifyCommit(c.subject)}] ${c.subject}${c.author ? ` \u2014 ${c.author}` : ""}`
|
|
2100
|
+
),
|
|
2101
|
+
``
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
if (opts.scoreBefore !== void 0 || opts.scoreAfter !== void 0) {
|
|
2105
|
+
lines.push(
|
|
2106
|
+
`## Architecture Health`,
|
|
2107
|
+
`Score at sprint start: ${opts.scoreBefore ?? "unknown"}`,
|
|
2108
|
+
`Score at sprint end: ${opts.scoreAfter ?? "unknown"}`,
|
|
2109
|
+
``
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
if (opts.amberContext) {
|
|
2113
|
+
const { capabilities, driftedCapabilities, summary } = opts.amberContext;
|
|
2114
|
+
lines.push(
|
|
2115
|
+
`## AMBER Capability Layer`,
|
|
2116
|
+
summary,
|
|
2117
|
+
`Drifted capabilities: ${driftedCapabilities}`,
|
|
2118
|
+
``,
|
|
2119
|
+
`### Capabilities`,
|
|
2120
|
+
...capabilities.slice(0, 15).map(
|
|
2121
|
+
(c) => `- ${c.name} [${c.criticality}/${c.lifecycle}]${c.driftCount > 0 ? ` \u26A0 drift: ${c.driftCount} files` : ""}`
|
|
2122
|
+
),
|
|
2123
|
+
``
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
if (opts.greenContext) {
|
|
2127
|
+
lines.push(
|
|
2128
|
+
`## GREEN Cross-Layer Insights`,
|
|
2129
|
+
opts.greenContext.summary,
|
|
2130
|
+
``
|
|
2131
|
+
);
|
|
2132
|
+
if (opts.greenContext.topRisks.length > 0) {
|
|
2133
|
+
lines.push(
|
|
2134
|
+
`### Top Risks`,
|
|
2135
|
+
...opts.greenContext.topRisks.slice(0, 5).map(
|
|
2136
|
+
(r) => `- ${r.name}: ${r.reason} (impact: ${r.impact}/10)`
|
|
2137
|
+
),
|
|
2138
|
+
``
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return lines.join("\n");
|
|
2143
|
+
}
|
|
2144
|
+
function buildSystemPrompt10(opts) {
|
|
2145
|
+
const toneMap = {
|
|
2146
|
+
professional: "You are a senior engineering manager facilitating a data-driven sprint retrospective. Be precise and actionable.",
|
|
2147
|
+
casual: "You are a friendly scrum master running a fun and honest retrospective. Keep it light but insightful.",
|
|
2148
|
+
"team-friendly": "You are a team lead who wants everyone to feel heard. Balance honesty with encouragement."
|
|
2149
|
+
};
|
|
2150
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
2151
|
+
}
|
|
2152
|
+
function buildUserPrompt10(opts) {
|
|
2153
|
+
const context = buildContext2(opts);
|
|
2154
|
+
const sprintName = opts.sprintName ?? "Current Sprint";
|
|
2155
|
+
return [
|
|
2156
|
+
`Generate a sprint retrospective for: ${opts.projectName} \u2014 ${sprintName}`,
|
|
2157
|
+
``,
|
|
2158
|
+
context,
|
|
2159
|
+
``,
|
|
2160
|
+
`## Output Format`,
|
|
2161
|
+
`Return a single JSON object with this exact shape:`,
|
|
2162
|
+
`{`,
|
|
2163
|
+
` "sprintName": "${sprintName}",`,
|
|
2164
|
+
` "period": "<date range or sprint identifier>",`,
|
|
2165
|
+
` "summary": "<2-3 sentence executive summary of the sprint>",`,
|
|
2166
|
+
` "delivered": [`,
|
|
2167
|
+
` { "item": "<what was built>", "capability": "<AMBER capability id if applicable>", "type": "feature|fix|refactor|chore" }`,
|
|
2168
|
+
` ],`,
|
|
2169
|
+
` "healthDelta": {`,
|
|
2170
|
+
` "scoreBefore": <number or null>,`,
|
|
2171
|
+
` "scoreAfter": <number or null>,`,
|
|
2172
|
+
` "delta": <number or null>,`,
|
|
2173
|
+
` "verdict": "improved|stable|degraded",`,
|
|
2174
|
+
` "degradedCapabilities": ["<capability name>"]`,
|
|
2175
|
+
` },`,
|
|
2176
|
+
` "wentWell": ["<positive thing 1>", ...],`,
|
|
2177
|
+
` "improvements": ["<improvement needed 1>", ...],`,
|
|
2178
|
+
` "puzzles": ["<question or confusion 1>", ...],`,
|
|
2179
|
+
` "actions": ["<concrete action item 1>", ...],`,
|
|
2180
|
+
` "debtIncurred": ["<technical debt item 1>", ...],`,
|
|
2181
|
+
` "slideTitle": "<1 sentence sprint summary for a slide>",`,
|
|
2182
|
+
` "slidePoints": ["<key point 1>", "<key point 2>", "<key point 3>"]`,
|
|
2183
|
+
`}`,
|
|
2184
|
+
``,
|
|
2185
|
+
`Rules:`,
|
|
2186
|
+
`- delivered: derive from git commits, 5\u201315 items`,
|
|
2187
|
+
`- wentWell: 3\u20135 genuine positives from the data`,
|
|
2188
|
+
`- improvements: 3\u20135 concrete improvements (not vague)`,
|
|
2189
|
+
`- puzzles: 2\u20134 things that were unclear or confusing this sprint`,
|
|
2190
|
+
`- actions: 3\u20135 specific, assigned-sounding action items`,
|
|
2191
|
+
`- debtIncurred: 1\u20134 technical debt items visible from the commits/drift`,
|
|
2192
|
+
`- slidePoints: exactly 3 bullet strings \u2264 80 characters`,
|
|
2193
|
+
`- Derive everything from the context above \u2014 no invented facts`,
|
|
2194
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
2195
|
+
].join("\n");
|
|
2196
|
+
}
|
|
2197
|
+
function computeHealthDelta(scoreBefore, scoreAfter, amberContext) {
|
|
2198
|
+
const before = scoreBefore ?? null;
|
|
2199
|
+
const after = scoreAfter ?? null;
|
|
2200
|
+
const delta = before !== null && after !== null ? after - before : null;
|
|
2201
|
+
let verdict = "stable";
|
|
2202
|
+
if (delta !== null) {
|
|
2203
|
+
if (delta > 2) verdict = "improved";
|
|
2204
|
+
else if (delta < -2) verdict = "degraded";
|
|
2205
|
+
}
|
|
2206
|
+
const degradedCapabilities = amberContext ? amberContext.capabilities.filter((c) => c.driftCount > 0 && (c.criticality === "critical" || c.criticality === "high")).map((c) => c.name) : [];
|
|
2207
|
+
return { scoreBefore: before, scoreAfter: after, delta, verdict, degradedCapabilities };
|
|
2208
|
+
}
|
|
2209
|
+
function buildFallback4(opts) {
|
|
2210
|
+
const sprintName = opts.sprintName ?? "Current Sprint";
|
|
2211
|
+
const commits = opts.gitContext?.commits ?? [];
|
|
2212
|
+
const healthDelta = computeHealthDelta(opts.scoreBefore, opts.scoreAfter, opts.amberContext);
|
|
2213
|
+
const delivered = commits.slice(0, 10).map((c) => ({
|
|
2214
|
+
item: c.subject,
|
|
2215
|
+
type: classifyCommit(c.subject)
|
|
2216
|
+
}));
|
|
2217
|
+
return {
|
|
2218
|
+
sprintName,
|
|
2219
|
+
period: opts.gitContext ? `${opts.gitContext.fromRef} \u2192 ${opts.gitContext.toRef}` : "This sprint",
|
|
2220
|
+
summary: `${opts.projectName} completed ${commits.length} commits this sprint across ${opts.gitContext?.filesChanged ?? 0} files.`,
|
|
2221
|
+
delivered,
|
|
2222
|
+
healthDelta,
|
|
2223
|
+
wentWell: [
|
|
2224
|
+
`${commits.filter((c) => classifyCommit(c.subject) === "feature").length} features shipped`,
|
|
2225
|
+
`${commits.filter((c) => classifyCommit(c.subject) === "fix").length} bugs fixed`,
|
|
2226
|
+
"Team maintained delivery cadence"
|
|
2227
|
+
],
|
|
2228
|
+
improvements: [
|
|
2229
|
+
"Reduce WIP by completing in-flight work before starting new",
|
|
2230
|
+
"Improve commit message clarity",
|
|
2231
|
+
"Address documentation drift in critical capabilities"
|
|
2232
|
+
],
|
|
2233
|
+
puzzles: [
|
|
2234
|
+
"Unclear ownership for cross-capability changes",
|
|
2235
|
+
"Test coverage strategy for new features"
|
|
2236
|
+
],
|
|
2237
|
+
actions: [
|
|
2238
|
+
"Schedule architecture review for flagged capabilities",
|
|
2239
|
+
"Set up automated coherence score tracking in CI",
|
|
2240
|
+
"Document top 3 architectural decisions made this sprint"
|
|
2241
|
+
],
|
|
2242
|
+
debtIncurred: healthDelta.degradedCapabilities.length > 0 ? healthDelta.degradedCapabilities.map((c) => `Documentation drift in ${c}`) : ["Review and address any undocumented capability changes"],
|
|
2243
|
+
slideTitle: `${sprintName}: ${commits.length} commits, ${opts.gitContext?.filesChanged ?? 0} files changed`,
|
|
2244
|
+
slidePoints: [
|
|
2245
|
+
`${commits.length} commits across ${opts.gitContext?.filesChanged ?? 0} files`,
|
|
2246
|
+
`Architecture health: ${healthDelta.verdict}`,
|
|
2247
|
+
`${healthDelta.degradedCapabilities.length} capabilities need attention`
|
|
2248
|
+
]
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function parseLlmResponse4(raw, opts) {
|
|
2252
|
+
let text = raw.trim();
|
|
2253
|
+
if (text.startsWith("```")) {
|
|
2254
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
2255
|
+
}
|
|
2256
|
+
let parsed;
|
|
2257
|
+
try {
|
|
2258
|
+
parsed = JSON.parse(text);
|
|
2259
|
+
} catch {
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
const sprintName = typeof parsed.sprintName === "string" ? parsed.sprintName : opts.sprintName ?? "Current Sprint";
|
|
2263
|
+
const period = typeof parsed.period === "string" ? parsed.period : "";
|
|
2264
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary : "";
|
|
2265
|
+
const rawDelivered = Array.isArray(parsed.delivered) ? parsed.delivered : [];
|
|
2266
|
+
const delivered = rawDelivered.filter((d) => typeof d.item === "string").map((d) => {
|
|
2267
|
+
const item = d;
|
|
2268
|
+
const type = ["feature", "fix", "refactor", "chore"].includes(item.type) ? item.type : "chore";
|
|
2269
|
+
return {
|
|
2270
|
+
item: item.item,
|
|
2271
|
+
...typeof item.capability === "string" ? { capability: item.capability } : {},
|
|
2272
|
+
type
|
|
2273
|
+
};
|
|
2274
|
+
});
|
|
2275
|
+
const hd = parsed.healthDelta ?? {};
|
|
2276
|
+
const scoreBeforeRaw = typeof hd.scoreBefore === "number" ? hd.scoreBefore : null;
|
|
2277
|
+
const scoreAfterRaw = typeof hd.scoreAfter === "number" ? hd.scoreAfter : null;
|
|
2278
|
+
const deltaRaw = typeof hd.delta === "number" ? hd.delta : null;
|
|
2279
|
+
const verdictRaw = ["improved", "stable", "degraded"].includes(hd.verdict) ? hd.verdict : "stable";
|
|
2280
|
+
const degradedCaps = Array.isArray(hd.degradedCapabilities) ? hd.degradedCapabilities.filter((x) => typeof x === "string") : [];
|
|
2281
|
+
const toStringArray = (key) => {
|
|
2282
|
+
const val = parsed[key];
|
|
2283
|
+
if (!Array.isArray(val)) return [];
|
|
2284
|
+
return val.filter((x) => typeof x === "string");
|
|
2285
|
+
};
|
|
2286
|
+
if (!summary && delivered.length === 0) return null;
|
|
2287
|
+
return {
|
|
2288
|
+
sprintName,
|
|
2289
|
+
period,
|
|
2290
|
+
summary,
|
|
2291
|
+
delivered,
|
|
2292
|
+
healthDelta: {
|
|
2293
|
+
scoreBefore: scoreBeforeRaw,
|
|
2294
|
+
scoreAfter: scoreAfterRaw,
|
|
2295
|
+
delta: deltaRaw,
|
|
2296
|
+
verdict: verdictRaw,
|
|
2297
|
+
degradedCapabilities: degradedCaps
|
|
2298
|
+
},
|
|
2299
|
+
wentWell: toStringArray("wentWell"),
|
|
2300
|
+
improvements: toStringArray("improvements"),
|
|
2301
|
+
puzzles: toStringArray("puzzles"),
|
|
2302
|
+
actions: toStringArray("actions"),
|
|
2303
|
+
debtIncurred: toStringArray("debtIncurred"),
|
|
2304
|
+
slideTitle: typeof parsed.slideTitle === "string" ? parsed.slideTitle : sprintName,
|
|
2305
|
+
slidePoints: toStringArray("slidePoints").slice(0, 3)
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
async function generateSprintRetro(opts) {
|
|
2309
|
+
try {
|
|
2310
|
+
const response = await opts.llm.complete({
|
|
2311
|
+
systemPrompt: buildSystemPrompt10(opts),
|
|
2312
|
+
messages: [{ role: "user", content: buildUserPrompt10(opts) }],
|
|
2313
|
+
maxTokens: 4096
|
|
2314
|
+
});
|
|
2315
|
+
const parsed = parseLlmResponse4(response.content, opts);
|
|
2316
|
+
if (parsed) return parsed;
|
|
2317
|
+
} catch {
|
|
2318
|
+
}
|
|
2319
|
+
return buildFallback4(opts);
|
|
2320
|
+
}
|
|
2321
|
+
|
|
2322
|
+
// src/generators/generateNewsletter.ts
|
|
2323
|
+
function buildContext3(opts) {
|
|
2324
|
+
const { digestContext: d, amberContext, projectName, teamName, targetAudience, tone } = opts;
|
|
2325
|
+
const lines = [
|
|
2326
|
+
`## Newsletter Context`,
|
|
2327
|
+
`Project: ${projectName}`,
|
|
2328
|
+
`Team: ${teamName ?? "Engineering"}`,
|
|
2329
|
+
`Target audience: ${targetAudience}`,
|
|
2330
|
+
`Tone: ${tone}`,
|
|
2331
|
+
``,
|
|
2332
|
+
`## Weekly Digest \u2014 ${d.period}`,
|
|
2333
|
+
`Commits: ${d.commitCount}`,
|
|
2334
|
+
`Files changed: ${d.filesChanged}`,
|
|
2335
|
+
`Coherence grade: ${d.grade ?? "N/A"}`,
|
|
2336
|
+
`Score start: ${d.scoreStart ?? "N/A"}`,
|
|
2337
|
+
`Score end: ${d.scoreEnd ?? "N/A"}`,
|
|
2338
|
+
`Score delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"}`,
|
|
2339
|
+
`Health summary: ${d.healthSummary}`,
|
|
2340
|
+
``
|
|
2341
|
+
];
|
|
2342
|
+
if (d.topCommits.length > 0) {
|
|
2343
|
+
lines.push(`### Top commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
|
|
2344
|
+
}
|
|
2345
|
+
if (d.newDrifts.length > 0) {
|
|
2346
|
+
lines.push(`### New capability drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
|
|
2347
|
+
}
|
|
2348
|
+
if (d.resolvedDrifts.length > 0) {
|
|
2349
|
+
lines.push(`### Resolved drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
|
|
2350
|
+
}
|
|
2351
|
+
if (d.newRisks.length > 0) {
|
|
2352
|
+
lines.push(`### New risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
2353
|
+
}
|
|
2354
|
+
if (amberContext) {
|
|
2355
|
+
lines.push(`## AMBER Capability Summary`, amberContext.summary, ``);
|
|
2356
|
+
}
|
|
2357
|
+
return lines.join("\n");
|
|
2358
|
+
}
|
|
2359
|
+
function buildSystemPrompt11(opts) {
|
|
2360
|
+
const audienceMap = {
|
|
2361
|
+
"product-team": "You are writing a newsletter for the product engineering team \u2014 technical details are welcome.",
|
|
2362
|
+
management: "You are writing a newsletter for engineering managers \u2014 focus on velocity, quality, and risk.",
|
|
2363
|
+
stakeholders: "You are writing a newsletter for business stakeholders \u2014 translate technical work into business outcomes.",
|
|
2364
|
+
"all-hands": "You are writing a company-wide newsletter \u2014 accessible to everyone, inspiring, and concise."
|
|
2365
|
+
};
|
|
2366
|
+
const toneMap = {
|
|
2367
|
+
professional: "Use a professional, data-driven tone.",
|
|
2368
|
+
casual: "Use a friendly, conversational tone \u2014 contractions welcome.",
|
|
2369
|
+
accessible: "Use zero technical jargon. If you must mention a technical concept, define it in plain language immediately."
|
|
2370
|
+
};
|
|
2371
|
+
return [
|
|
2372
|
+
audienceMap[opts.targetAudience],
|
|
2373
|
+
toneMap[opts.tone],
|
|
2374
|
+
"Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object."
|
|
2375
|
+
].join(" ");
|
|
2376
|
+
}
|
|
2377
|
+
function buildUserPrompt11(opts) {
|
|
2378
|
+
const context = buildContext3(opts);
|
|
2379
|
+
const { projectName, teamName, targetAudience, tone, includeMetrics } = opts;
|
|
2380
|
+
return [
|
|
2381
|
+
`Generate a weekly engineering newsletter for: ${projectName}`,
|
|
2382
|
+
`Team: ${teamName ?? "Engineering"} | Audience: ${targetAudience} | Tone: ${tone}`,
|
|
2383
|
+
`Include metrics: ${includeMetrics}`,
|
|
2384
|
+
``,
|
|
2385
|
+
context,
|
|
2386
|
+
``,
|
|
2387
|
+
`## Output Format`,
|
|
2388
|
+
`Return a single JSON object with this exact shape:`,
|
|
2389
|
+
`{`,
|
|
2390
|
+
` "subject": "<email subject line, \u226460 chars>",`,
|
|
2391
|
+
` "preview": "<email preview text, \u226450 chars>",`,
|
|
2392
|
+
` "greeting": "<opening greeting>",`,
|
|
2393
|
+
` "headline": "<1-sentence hook that opens the newsletter>",`,
|
|
2394
|
+
` "sections": [`,
|
|
2395
|
+
` { "heading": "<section heading>", "body": "<2-4 sentences>", "type": "highlight|metrics|risk|shoutout|upcoming" }`,
|
|
2396
|
+
` ],`,
|
|
2397
|
+
` "metrics": [`,
|
|
2398
|
+
` { "label": "<metric name>", "value": "<value>", "trend": "up|down|stable" }`,
|
|
2399
|
+
` ],`,
|
|
2400
|
+
` "closing": "<1-2 sentence closing>",`,
|
|
2401
|
+
` "unsubscribeNote": "Reply STOP to unsubscribe from this digest.",`,
|
|
2402
|
+
` "slackVersion": "<Slack mrkdwn formatted version of the newsletter>",`,
|
|
2403
|
+
` "teamsVersion": "<Microsoft Teams Adaptive Card JSON as a string>",`,
|
|
2404
|
+
` "htmlVersion": "<complete HTML email with inline styles, dark-mode friendly>"`,
|
|
2405
|
+
`}`,
|
|
2406
|
+
``,
|
|
2407
|
+
`Rules:`,
|
|
2408
|
+
`- sections: 3\u20135 sections covering the highlights`,
|
|
2409
|
+
`- metrics: ${includeMetrics ? "include 3\u20135 key metrics from the digest" : "return empty array []"}`,
|
|
2410
|
+
`- slackVersion: use *bold*, _italic_, \`code\`, > blockquote, \u2022 bullets (mrkdwn)`,
|
|
2411
|
+
`- teamsVersion: valid Teams Adaptive Card JSON with TextBlock, FactSet elements`,
|
|
2412
|
+
`- htmlVersion: full HTML with <html><head><body>, inline CSS, no external resources`,
|
|
2413
|
+
`- subject: must reference the week/project and be compelling`,
|
|
2414
|
+
`- Derive all content from the digest context above \u2014 no invented facts`,
|
|
2415
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
2416
|
+
].join("\n");
|
|
2417
|
+
}
|
|
2418
|
+
function buildHtmlEmail(opts, sections, metrics) {
|
|
2419
|
+
const { projectName, teamName, digestContext: d } = opts;
|
|
2420
|
+
const sectionHtml = sections.map((s) => `
|
|
2421
|
+
<tr><td style="padding:20px 32px 0">
|
|
2422
|
+
<h2 style="margin:0 0 8px;font-size:16px;color:#e2e8f0">${s.heading}</h2>
|
|
2423
|
+
<p style="margin:0;color:#94a3b8;font-size:14px;line-height:1.6">${s.body}</p>
|
|
2424
|
+
</td></tr>`).join("");
|
|
2425
|
+
const metricsHtml = metrics.length > 0 ? `
|
|
2426
|
+
<tr><td style="padding:20px 32px 0">
|
|
2427
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
2428
|
+
<tr>${metrics.map((m) => `
|
|
2429
|
+
<td style="text-align:center;padding:12px;background:#1e293b;border-radius:8px;margin:4px">
|
|
2430
|
+
<div style="font-size:22px;font-weight:700;color:${m.trend === "up" ? "#34d399" : m.trend === "down" ? "#f87171" : "#94a3b8"}">${m.value}</div>
|
|
2431
|
+
<div style="font-size:12px;color:#64748b;margin-top:4px">${m.label}</div>
|
|
2432
|
+
</td>`).join("<td style='width:8px'></td>")}</tr>
|
|
2433
|
+
</table>
|
|
2434
|
+
</td></tr>` : "";
|
|
2435
|
+
return `<!DOCTYPE html>
|
|
2436
|
+
<html lang="en">
|
|
2437
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${projectName} \u2014 Weekly Digest</title></head>
|
|
2438
|
+
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
|
2439
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
|
|
2440
|
+
<tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
|
|
2441
|
+
<h1 style="margin:0;font-size:24px;font-weight:700;color:#f1f5f9">${projectName}</h1>
|
|
2442
|
+
<p style="margin:4px 0 0;color:#64748b;font-size:14px">${teamName ?? "Engineering"} \xB7 ${d.period}</p>
|
|
2443
|
+
</td></tr>
|
|
2444
|
+
${sectionHtml}
|
|
2445
|
+
${metricsHtml}
|
|
2446
|
+
<tr><td style="padding:24px 32px;border-top:1px solid #1e293b;margin-top:24px">
|
|
2447
|
+
<p style="margin:0;color:#475569;font-size:12px">Generated by forge0x2B \xB7 Reply STOP to unsubscribe.</p>
|
|
2448
|
+
</td></tr>
|
|
2449
|
+
</table>
|
|
2450
|
+
</body>
|
|
2451
|
+
</html>`;
|
|
2452
|
+
}
|
|
2453
|
+
function buildSlackVersion(opts, sections, metrics) {
|
|
2454
|
+
const { projectName, digestContext: d } = opts;
|
|
2455
|
+
const lines = [
|
|
2456
|
+
`*${projectName} \u2014 Weekly Engineering Digest*`,
|
|
2457
|
+
`_${d.period}_`,
|
|
2458
|
+
``,
|
|
2459
|
+
`*Health:* ${d.healthSummary}`,
|
|
2460
|
+
``,
|
|
2461
|
+
...sections.map((s) => [`*${s.heading}*`, s.body, ""].join("\n"))
|
|
2462
|
+
];
|
|
2463
|
+
if (metrics.length > 0) {
|
|
2464
|
+
lines.push(
|
|
2465
|
+
`*Key Metrics*`,
|
|
2466
|
+
...metrics.map((m) => `\u2022 *${m.label}:* ${m.value} ${m.trend === "up" ? "\u2191" : m.trend === "down" ? "\u2193" : "\u2192"}`),
|
|
2467
|
+
``
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
lines.push(`_Generated by forge0x2B_`);
|
|
2471
|
+
return lines.join("\n");
|
|
2472
|
+
}
|
|
2473
|
+
function buildTeamsVersion(opts, sections) {
|
|
2474
|
+
const { projectName, digestContext: d } = opts;
|
|
2475
|
+
const card = {
|
|
2476
|
+
type: "AdaptiveCard",
|
|
2477
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
2478
|
+
version: "1.4",
|
|
2479
|
+
body: [
|
|
2480
|
+
{
|
|
2481
|
+
type: "TextBlock",
|
|
2482
|
+
text: `${projectName} \u2014 Weekly Engineering Digest`,
|
|
2483
|
+
weight: "Bolder",
|
|
2484
|
+
size: "Large",
|
|
2485
|
+
color: "Accent"
|
|
2486
|
+
},
|
|
2487
|
+
{
|
|
2488
|
+
type: "TextBlock",
|
|
2489
|
+
text: d.period,
|
|
2490
|
+
isSubtle: true,
|
|
2491
|
+
spacing: "None"
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
type: "TextBlock",
|
|
2495
|
+
text: d.healthSummary,
|
|
2496
|
+
wrap: true,
|
|
2497
|
+
spacing: "Medium"
|
|
2498
|
+
},
|
|
2499
|
+
...sections.slice(0, 3).map((s) => ({
|
|
2500
|
+
type: "Container",
|
|
2501
|
+
spacing: "Medium",
|
|
2502
|
+
items: [
|
|
2503
|
+
{ type: "TextBlock", text: s.heading, weight: "Bolder", wrap: true },
|
|
2504
|
+
{ type: "TextBlock", text: s.body, wrap: true, isSubtle: true }
|
|
2505
|
+
]
|
|
2506
|
+
})),
|
|
2507
|
+
{
|
|
2508
|
+
type: "TextBlock",
|
|
2509
|
+
text: "Generated by forge0x2B",
|
|
2510
|
+
isSubtle: true,
|
|
2511
|
+
size: "Small",
|
|
2512
|
+
spacing: "Large"
|
|
2513
|
+
}
|
|
2514
|
+
]
|
|
2515
|
+
};
|
|
2516
|
+
return JSON.stringify(card);
|
|
2517
|
+
}
|
|
2518
|
+
function buildFallback5(opts) {
|
|
2519
|
+
const { projectName, teamName, digestContext: d, includeMetrics } = opts;
|
|
2520
|
+
const sections = [
|
|
2521
|
+
{
|
|
2522
|
+
heading: "This Week in Engineering",
|
|
2523
|
+
body: `The team shipped ${d.commitCount} commits across ${d.filesChanged} files during ${d.period}. ${d.healthSummary}`,
|
|
2524
|
+
type: "highlight"
|
|
2525
|
+
}
|
|
2526
|
+
];
|
|
2527
|
+
if (d.topCommits.length > 0) {
|
|
2528
|
+
sections.push({
|
|
2529
|
+
heading: "Highlights",
|
|
2530
|
+
body: d.topCommits.slice(0, 3).join(". ") + ".",
|
|
2531
|
+
type: "highlight"
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
if (d.newRisks.length > 0) {
|
|
2535
|
+
sections.push({
|
|
2536
|
+
heading: "On Our Radar",
|
|
2537
|
+
body: `We identified ${d.newRisks.length} new risk${d.newRisks.length > 1 ? "s" : ""} this week: ${d.newRisks.slice(0, 2).join(", ")}.`,
|
|
2538
|
+
type: "risk"
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
const metrics = includeMetrics ? [
|
|
2542
|
+
{ label: "Commits", value: String(d.commitCount), trend: "stable" },
|
|
2543
|
+
{ label: "Files Changed", value: String(d.filesChanged), trend: "stable" },
|
|
2544
|
+
...d.grade ? [{ label: "Architecture Grade", value: d.grade, trend: d.scoreDelta !== null && d.scoreDelta > 0 ? "up" : d.scoreDelta !== null && d.scoreDelta < 0 ? "down" : "stable" }] : []
|
|
2545
|
+
] : [];
|
|
2546
|
+
return {
|
|
2547
|
+
subject: `${projectName} Engineering Digest \u2014 ${d.period}`,
|
|
2548
|
+
preview: `${d.commitCount} commits \xB7 ${d.healthSummary.slice(0, 40)}`,
|
|
2549
|
+
greeting: `Hello ${teamName ?? "team"},`,
|
|
2550
|
+
headline: `Here's what the engineering team shipped this week.`,
|
|
2551
|
+
sections,
|
|
2552
|
+
metrics,
|
|
2553
|
+
closing: `Thanks for reading. See you next week!`,
|
|
2554
|
+
unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
|
|
2555
|
+
slackVersion: buildSlackVersion(opts, sections, metrics),
|
|
2556
|
+
teamsVersion: buildTeamsVersion(opts, sections),
|
|
2557
|
+
htmlVersion: buildHtmlEmail(opts, sections, metrics)
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
function parseLlmResponse5(raw, opts) {
|
|
2561
|
+
let text = raw.trim();
|
|
2562
|
+
if (text.startsWith("```")) {
|
|
2563
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
2564
|
+
}
|
|
2565
|
+
let parsed;
|
|
2566
|
+
try {
|
|
2567
|
+
parsed = JSON.parse(text);
|
|
2568
|
+
} catch {
|
|
2569
|
+
return null;
|
|
2570
|
+
}
|
|
2571
|
+
const getString = (key, fallback) => typeof parsed[key] === "string" ? parsed[key] : fallback;
|
|
2572
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
2573
|
+
const sections = rawSections.filter((s) => typeof s.heading === "string").map((s) => {
|
|
2574
|
+
const item = s;
|
|
2575
|
+
return {
|
|
2576
|
+
heading: item.heading,
|
|
2577
|
+
body: typeof item.body === "string" ? item.body : "",
|
|
2578
|
+
type: ["highlight", "metrics", "risk", "shoutout", "upcoming"].includes(item.type) ? item.type : "highlight"
|
|
2579
|
+
};
|
|
2580
|
+
});
|
|
2581
|
+
const rawMetrics = Array.isArray(parsed.metrics) ? parsed.metrics : [];
|
|
2582
|
+
const metrics = rawMetrics.filter((m) => typeof m.label === "string").map((m) => {
|
|
2583
|
+
const item = m;
|
|
2584
|
+
return {
|
|
2585
|
+
label: item.label,
|
|
2586
|
+
value: typeof item.value === "string" ? item.value : "",
|
|
2587
|
+
trend: ["up", "down", "stable"].includes(item.trend) ? item.trend : "stable"
|
|
2588
|
+
};
|
|
2589
|
+
});
|
|
2590
|
+
const subject = getString("subject", "");
|
|
2591
|
+
if (!subject) return null;
|
|
2592
|
+
const slackVersion = getString("slackVersion", "") || buildSlackVersion(opts, sections, metrics);
|
|
2593
|
+
const teamsVersion = getString("teamsVersion", "") || buildTeamsVersion(opts, sections);
|
|
2594
|
+
const htmlVersion = getString("htmlVersion", "") || buildHtmlEmail(opts, sections, metrics);
|
|
2595
|
+
return {
|
|
2596
|
+
subject,
|
|
2597
|
+
preview: getString("preview", "").slice(0, 50),
|
|
2598
|
+
greeting: getString("greeting", `Hello ${opts.teamName ?? "team"},`),
|
|
2599
|
+
headline: getString("headline", ""),
|
|
2600
|
+
sections,
|
|
2601
|
+
metrics,
|
|
2602
|
+
closing: getString("closing", ""),
|
|
2603
|
+
unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
|
|
2604
|
+
slackVersion,
|
|
2605
|
+
teamsVersion,
|
|
2606
|
+
htmlVersion
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
async function generateNewsletter(opts) {
|
|
2610
|
+
try {
|
|
2611
|
+
const response = await opts.llm.complete({
|
|
2612
|
+
systemPrompt: buildSystemPrompt11(opts),
|
|
2613
|
+
messages: [{ role: "user", content: buildUserPrompt11(opts) }],
|
|
2614
|
+
maxTokens: 8192
|
|
2615
|
+
});
|
|
2616
|
+
const parsed = parseLlmResponse5(response.content, opts);
|
|
2617
|
+
if (parsed) return parsed;
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
return buildFallback5(opts);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// src/generators/generateRadio.ts
|
|
2624
|
+
function buildContext4(opts) {
|
|
2625
|
+
const { digestContext: d, amberContext, projectName, audience } = opts;
|
|
2626
|
+
const lines = [
|
|
2627
|
+
`## Architecture Radio Context`,
|
|
2628
|
+
`Project: ${projectName}`,
|
|
2629
|
+
`Audience: ${audience}`,
|
|
2630
|
+
`Period: ${d.period}`,
|
|
2631
|
+
``,
|
|
2632
|
+
`## Weekly Digest`,
|
|
2633
|
+
`Commits: ${d.commitCount}`,
|
|
2634
|
+
`Files changed: ${d.filesChanged}`,
|
|
2635
|
+
`Coherence grade: ${d.grade ?? "N/A"}`,
|
|
2636
|
+
`Score: ${d.scoreStart ?? "N/A"} \u2192 ${d.scoreEnd ?? "N/A"} (delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"})`,
|
|
2637
|
+
`Health: ${d.healthSummary}`,
|
|
2638
|
+
``
|
|
2639
|
+
];
|
|
2640
|
+
if (d.topCommits.length > 0) {
|
|
2641
|
+
lines.push(`### Top Commits`, ...d.topCommits.slice(0, 5).map((c) => `- ${c}`), ``);
|
|
2642
|
+
}
|
|
2643
|
+
if (d.newDrifts.length > 0) {
|
|
2644
|
+
lines.push(`### Capability Drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
|
|
2645
|
+
}
|
|
2646
|
+
if (d.resolvedDrifts.length > 0) {
|
|
2647
|
+
lines.push(`### Resolved Drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
|
|
2648
|
+
}
|
|
2649
|
+
if (d.newRisks.length > 0) {
|
|
2650
|
+
lines.push(`### New Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
2651
|
+
}
|
|
2652
|
+
if (amberContext) {
|
|
2653
|
+
lines.push(
|
|
2654
|
+
`## AMBER Capability Summary`,
|
|
2655
|
+
`Total capabilities: ${amberContext.capabilities.length}`,
|
|
2656
|
+
`Drifted: ${amberContext.driftedCapabilities}`,
|
|
2657
|
+
`Tagged files: ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`,
|
|
2658
|
+
``,
|
|
2659
|
+
amberContext.summary,
|
|
2660
|
+
``
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
2663
|
+
return lines.join("\n");
|
|
2664
|
+
}
|
|
2665
|
+
function buildSystemPrompt12(audience) {
|
|
2666
|
+
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.";
|
|
2667
|
+
return [
|
|
2668
|
+
`You are Architecture Radio \u2014 the daily briefing that turns raw engineering signal into clear, compelling communication.`,
|
|
2669
|
+
audienceInstructions,
|
|
2670
|
+
`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.`,
|
|
2671
|
+
`Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
|
|
2672
|
+
].join(" ");
|
|
2673
|
+
}
|
|
2674
|
+
function buildUserPrompt12(opts) {
|
|
2675
|
+
const context = buildContext4(opts);
|
|
2676
|
+
return [
|
|
2677
|
+
`Generate today's Architecture Radio briefing for: ${opts.projectName}`,
|
|
2678
|
+
`Audience: ${opts.audience}`,
|
|
2679
|
+
``,
|
|
2680
|
+
context,
|
|
2681
|
+
``,
|
|
2682
|
+
`## Output Format`,
|
|
2683
|
+
`Return a single JSON object with this exact shape:`,
|
|
2684
|
+
`{`,
|
|
2685
|
+
` "headline": "<1 powerful sentence capturing the most important thing \u2014 not a title, a statement>",`,
|
|
2686
|
+
` "slackVersion": "<~150 word Slack mrkdwn message \u2014 punchy, action-oriented, uses *bold*, _italic_, \u2022 bullets>",`,
|
|
2687
|
+
` "emailVersion": "<~400 word complete HTML email \u2014 full <html><head><body> with inline CSS, dark background, proper greeting, body paragraphs, clear CTA>",`,
|
|
2688
|
+
` "twitterVersion": "<insight worth sharing publicly \u2014 \u2264280 chars, no sensitive data, no project-specific names unless generic>"`,
|
|
2689
|
+
`}`,
|
|
2690
|
+
``,
|
|
2691
|
+
`Rules:`,
|
|
2692
|
+
`- headline: not a generic recap \u2014 capture what MATTERS most today`,
|
|
2693
|
+
`- slackVersion: start with the headline, use mrkdwn formatting`,
|
|
2694
|
+
`- emailVersion: full HTML with inline styles, dark-mode friendly (#0f172a background), greeting, 2-3 paragraphs, bold CTA at end`,
|
|
2695
|
+
`- twitterVersion: shareable engineering insight, safe for public, abstract enough for any team`,
|
|
2696
|
+
`- ${opts.audience === "executive" ? "No technical jargon anywhere \u2014 business language only" : "Be technically precise \u2014 engineers hate vague"}`,
|
|
2697
|
+
`- Derive all content from the digest context \u2014 no invented facts`,
|
|
2698
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
2699
|
+
].join("\n");
|
|
2700
|
+
}
|
|
2701
|
+
function buildFallback6(opts) {
|
|
2702
|
+
const { digestContext: d, projectName, audience } = opts;
|
|
2703
|
+
const isExec = audience === "executive";
|
|
2704
|
+
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}`;
|
|
2705
|
+
const slackVersion = [
|
|
2706
|
+
`*\u{1F4E1} Architecture Radio \u2014 ${d.period}*`,
|
|
2707
|
+
``,
|
|
2708
|
+
`*${headline}*`,
|
|
2709
|
+
``,
|
|
2710
|
+
`\u2022 *Health:* ${d.healthSummary}`,
|
|
2711
|
+
d.commitCount > 0 ? `\u2022 *Commits:* ${d.commitCount} across ${d.filesChanged} files` : "",
|
|
2712
|
+
d.newDrifts.length > 0 ? `\u2022 *Drifts:* ${d.newDrifts.slice(0, 3).join(", ")}` : "",
|
|
2713
|
+
d.newRisks.length > 0 ? `\u2022 *Risks:* ${d.newRisks.slice(0, 2).join(", ")}` : "",
|
|
2714
|
+
``,
|
|
2715
|
+
`_Generated by forge0x2B_`
|
|
2716
|
+
].filter(Boolean).join("\n");
|
|
2717
|
+
const emailVersion = `<!DOCTYPE html>
|
|
2718
|
+
<html lang="en">
|
|
2719
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Architecture Radio \u2014 ${projectName}</title></head>
|
|
2720
|
+
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
|
2721
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
|
|
2722
|
+
<tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
|
|
2723
|
+
<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>
|
|
2724
|
+
<h1 style="margin:0;font-size:22px;font-weight:700;color:#f1f5f9;line-height:1.3">${headline}</h1>
|
|
2725
|
+
<p style="margin:8px 0 0;color:#64748b;font-size:13px">${d.period}</p>
|
|
2726
|
+
</td></tr>
|
|
2727
|
+
<tr><td style="padding:24px 32px">
|
|
2728
|
+
<p style="margin:0 0 16px;color:#94a3b8;font-size:14px;line-height:1.6">Hello,</p>
|
|
2729
|
+
<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>
|
|
2730
|
+
${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>` : ""}
|
|
2731
|
+
${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>` : ""}
|
|
2732
|
+
<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>
|
|
2733
|
+
</td></tr>
|
|
2734
|
+
</table>
|
|
2735
|
+
</body>
|
|
2736
|
+
</html>`;
|
|
2737
|
+
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}`;
|
|
2738
|
+
return { headline, slackVersion, emailVersion, twitterVersion };
|
|
2739
|
+
}
|
|
2740
|
+
function parseLlmResponse6(raw) {
|
|
2741
|
+
let text = raw.trim();
|
|
2742
|
+
if (text.startsWith("```")) {
|
|
2743
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
2744
|
+
}
|
|
2745
|
+
let parsed;
|
|
2746
|
+
try {
|
|
2747
|
+
parsed = JSON.parse(text);
|
|
2748
|
+
} catch {
|
|
2749
|
+
return null;
|
|
2750
|
+
}
|
|
2751
|
+
const getString = (key) => typeof parsed[key] === "string" ? parsed[key] : "";
|
|
2752
|
+
const headline = getString("headline");
|
|
2753
|
+
if (!headline) return null;
|
|
2754
|
+
return {
|
|
2755
|
+
headline,
|
|
2756
|
+
slackVersion: getString("slackVersion"),
|
|
2757
|
+
emailVersion: getString("emailVersion"),
|
|
2758
|
+
twitterVersion: getString("twitterVersion").slice(0, 280)
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
async function generateRadio(options) {
|
|
2762
|
+
try {
|
|
2763
|
+
const response = await options.llm.complete({
|
|
2764
|
+
systemPrompt: buildSystemPrompt12(options.audience),
|
|
2765
|
+
messages: [{ role: "user", content: buildUserPrompt12(options) }],
|
|
2766
|
+
maxTokens: 4096
|
|
2767
|
+
});
|
|
2768
|
+
const parsed = parseLlmResponse6(response.content);
|
|
2769
|
+
if (parsed) return parsed;
|
|
2770
|
+
} catch {
|
|
2771
|
+
}
|
|
2772
|
+
return buildFallback6(options);
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
// src/generators/generateArc42.ts
|
|
2776
|
+
var ARC42_ATTRIBUTION = `arc42 template \xA9 arc42.org, Creative Commons Attribution 4.0 International (CC-BY 4.0).
|
|
2777
|
+
See https://arc42.org for the original template and documentation.`;
|
|
2778
|
+
function buildSection5(opts) {
|
|
2779
|
+
const { amberContext, projectName } = opts;
|
|
2780
|
+
if (!amberContext || amberContext.capabilities.length === 0) {
|
|
2781
|
+
return `## 5. Building Block View
|
|
2782
|
+
|
|
2783
|
+
No AMBER capability data available. Run an AMBER scan to populate this section.
|
|
2784
|
+
|
|
2785
|
+
*This section would show the top-level decomposition of the system into building blocks (capabilities) and their relationships.*
|
|
2786
|
+
`;
|
|
2787
|
+
}
|
|
2788
|
+
const lines = [
|
|
2789
|
+
`## 5. Building Block View`,
|
|
2790
|
+
``,
|
|
2791
|
+
`*Source: PRISM AMBER capability registry*`,
|
|
2792
|
+
``,
|
|
2793
|
+
`### Level 1 \u2014 ${projectName} (Whitebox)`,
|
|
2794
|
+
``,
|
|
2795
|
+
`**Total capabilities:** ${amberContext.capabilities.length}`,
|
|
2796
|
+
`**Tagged files:** ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent.toFixed(0)}%)`,
|
|
2797
|
+
``,
|
|
2798
|
+
`| Capability | Criticality | Lifecycle | Files |`,
|
|
2799
|
+
`|------------|-------------|-----------|-------|`
|
|
2800
|
+
];
|
|
2801
|
+
for (const cap of amberContext.capabilities) {
|
|
2802
|
+
lines.push(
|
|
2803
|
+
`| ${cap.name} | ${cap.criticality} | ${cap.lifecycle} | ${cap.files.length} |`
|
|
2804
|
+
);
|
|
2805
|
+
}
|
|
2806
|
+
if (amberContext.driftedCapabilities > 0) {
|
|
2807
|
+
lines.push(
|
|
2808
|
+
``,
|
|
2809
|
+
`> **Note:** ${amberContext.driftedCapabilities} capabilities have documentation drift (files modified since last doc update).`
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2812
|
+
if (amberContext.orphanedFiles.length > 0) {
|
|
2813
|
+
lines.push(
|
|
2814
|
+
``,
|
|
2815
|
+
`### Orphaned Files (not assigned to any capability)`,
|
|
2816
|
+
``,
|
|
2817
|
+
...amberContext.orphanedFiles.slice(0, 10).map((f) => `- \`${f}\``),
|
|
2818
|
+
amberContext.orphanedFiles.length > 10 ? `- *... and ${amberContext.orphanedFiles.length - 10} more*` : ``
|
|
2819
|
+
);
|
|
2820
|
+
}
|
|
2821
|
+
return lines.join("\n");
|
|
2822
|
+
}
|
|
2823
|
+
function buildSection9(opts) {
|
|
2824
|
+
const { digestContext: d } = opts;
|
|
2825
|
+
const lines = [
|
|
2826
|
+
`## 9. Architecture Decisions`,
|
|
2827
|
+
``,
|
|
2828
|
+
`*Source: PRISM architecture signals*`,
|
|
2829
|
+
``
|
|
2830
|
+
];
|
|
2831
|
+
if (d.newRisks.length > 0) {
|
|
2832
|
+
lines.push(
|
|
2833
|
+
`### Recent Architectural Risks Surfaced`,
|
|
2834
|
+
``,
|
|
2835
|
+
...d.newRisks.map((r) => `- ${r}`),
|
|
2836
|
+
``
|
|
2837
|
+
);
|
|
2838
|
+
}
|
|
2839
|
+
if (d.newDrifts.length > 0 || d.resolvedDrifts.length > 0) {
|
|
2840
|
+
lines.push(
|
|
2841
|
+
`### Documentation State`,
|
|
2842
|
+
``
|
|
2843
|
+
);
|
|
2844
|
+
if (d.newDrifts.length > 0) {
|
|
2845
|
+
lines.push(
|
|
2846
|
+
`**New capability drifts** (${d.newDrifts.length}):`,
|
|
2847
|
+
...d.newDrifts.map((c) => `- ${c}`),
|
|
2848
|
+
``
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
if (d.resolvedDrifts.length > 0) {
|
|
2852
|
+
lines.push(
|
|
2853
|
+
`**Resolved drifts** (${d.resolvedDrifts.length}):`,
|
|
2854
|
+
...d.resolvedDrifts.map((c) => `- \u2713 ${c}`),
|
|
2855
|
+
``
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
lines.push(
|
|
2860
|
+
`### Placeholder`,
|
|
2861
|
+
``,
|
|
2862
|
+
`*Add ADRs (Architecture Decision Records) here. Use the forge0x2B ADR generator to produce MADR-format decisions from your codebase.*`,
|
|
2863
|
+
``,
|
|
2864
|
+
`| ID | Title | Status | Date |`,
|
|
2865
|
+
`|----|-------|--------|------|`,
|
|
2866
|
+
`| ADR-001 | *Example decision* | Proposed | ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} |`
|
|
2867
|
+
);
|
|
2868
|
+
return lines.join("\n");
|
|
2869
|
+
}
|
|
2870
|
+
function buildSection10(opts) {
|
|
2871
|
+
const { digestContext: d } = opts;
|
|
2872
|
+
const lines = [
|
|
2873
|
+
`## 10. Quality Requirements`,
|
|
2874
|
+
``,
|
|
2875
|
+
`*Source: PRISM coherence metrics*`,
|
|
2876
|
+
``,
|
|
2877
|
+
`### Quality Tree`,
|
|
2878
|
+
``,
|
|
2879
|
+
`| Quality Goal | Scenario | Priority | Current State |`,
|
|
2880
|
+
`|--------------|----------|----------|---------------|`
|
|
2881
|
+
];
|
|
2882
|
+
const grade = d.grade ?? "N/A";
|
|
2883
|
+
const gradeColor = grade.startsWith("A") ? "\u2713" : grade.startsWith("B") ? "~" : "\u26A0";
|
|
2884
|
+
lines.push(
|
|
2885
|
+
`| Architecture Coherence | Architecture grade \u2265 B | High | ${gradeColor} ${grade} |`,
|
|
2886
|
+
`| Documentation Coverage | All capabilities documented | Medium | ${d.newDrifts.length === 0 ? "\u2713 No drift" : `\u26A0 ${d.newDrifts.length} drifted`} |`,
|
|
2887
|
+
`| Change Stability | Low churn per capability | Medium | ${d.filesChanged} files changed this period |`
|
|
2888
|
+
);
|
|
2889
|
+
if (d.scoreStart !== null && d.scoreEnd !== null) {
|
|
2890
|
+
const delta = d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A";
|
|
2891
|
+
lines.push(
|
|
2892
|
+
``,
|
|
2893
|
+
`### Coherence Score Trend`,
|
|
2894
|
+
``,
|
|
2895
|
+
`| Period | Start | End | Delta |`,
|
|
2896
|
+
`|--------|-------|-----|-------|`,
|
|
2897
|
+
`| ${d.period} | ${d.scoreStart} | ${d.scoreEnd} | ${delta} |`
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
return lines.join("\n");
|
|
2901
|
+
}
|
|
2902
|
+
function buildSection11(opts) {
|
|
2903
|
+
const { digestContext: d, amberContext } = opts;
|
|
2904
|
+
const lines = [
|
|
2905
|
+
`## 11. Risks and Technical Debt`,
|
|
2906
|
+
``,
|
|
2907
|
+
`*Source: PRISM risk signals*`,
|
|
2908
|
+
``,
|
|
2909
|
+
`### Risk Register`,
|
|
2910
|
+
``,
|
|
2911
|
+
`| Risk | Probability | Impact | Mitigation |`,
|
|
2912
|
+
`|------|------------|--------|------------|`
|
|
2913
|
+
];
|
|
2914
|
+
if (d.newRisks.length > 0) {
|
|
2915
|
+
for (const risk of d.newRisks) {
|
|
2916
|
+
lines.push(`| ${risk} | Medium | Medium | Review and address in next sprint |`);
|
|
2917
|
+
}
|
|
2918
|
+
} else {
|
|
2919
|
+
lines.push(`| *No new risks detected in ${d.period}* | \u2014 | \u2014 | \u2014 |`);
|
|
2920
|
+
}
|
|
2921
|
+
if (amberContext && amberContext.driftedCapabilities > 0) {
|
|
2922
|
+
lines.push(`| Documentation drift: ${amberContext.driftedCapabilities} capabilities | High | Low | Run \`prism sync\` |`);
|
|
2923
|
+
}
|
|
2924
|
+
if (d.newDrifts.length > 0) {
|
|
2925
|
+
lines.push(
|
|
2926
|
+
``,
|
|
2927
|
+
`### Technical Debt \u2014 Documentation Drift`,
|
|
2928
|
+
``,
|
|
2929
|
+
`The following capabilities have drifted documentation (code changed since last doc update):`,
|
|
2930
|
+
``,
|
|
2931
|
+
...d.newDrifts.map((c) => `- \`${c}\``)
|
|
2932
|
+
);
|
|
2933
|
+
}
|
|
2934
|
+
lines.push(
|
|
2935
|
+
``,
|
|
2936
|
+
`### Health Summary`,
|
|
2937
|
+
``,
|
|
2938
|
+
`> ${d.healthSummary}`
|
|
2939
|
+
);
|
|
2940
|
+
return lines.join("\n");
|
|
2941
|
+
}
|
|
2942
|
+
function buildSection12(opts) {
|
|
2943
|
+
const { amberContext, projectName } = opts;
|
|
2944
|
+
const lines = [
|
|
2945
|
+
`## 12. Glossary`,
|
|
2946
|
+
``,
|
|
2947
|
+
`*Source: PRISM AMBER capability registry (Ubiquitous Language)*`,
|
|
2948
|
+
``,
|
|
2949
|
+
`| Term | Definition |`,
|
|
2950
|
+
`|------|------------|`
|
|
2951
|
+
];
|
|
2952
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
2953
|
+
for (const cap of amberContext.capabilities) {
|
|
2954
|
+
const desc = cap.description ? cap.description : `${cap.criticality} ${cap.lifecycle} capability in ${projectName}.`;
|
|
2955
|
+
lines.push(`| **${cap.name}** | ${desc} |`);
|
|
2956
|
+
}
|
|
2957
|
+
} else {
|
|
2958
|
+
lines.push(
|
|
2959
|
+
`| *No capabilities registered yet* | Run an AMBER scan to populate the ubiquitous language. |`
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
lines.push(
|
|
2963
|
+
``,
|
|
2964
|
+
`*Add domain-specific terms and acronyms below as the glossary evolves.*`
|
|
2965
|
+
);
|
|
2966
|
+
return lines.join("\n");
|
|
2967
|
+
}
|
|
2968
|
+
function buildSection2() {
|
|
2969
|
+
return `## 2. Constraints
|
|
2970
|
+
|
|
2971
|
+
*Fill in the constraints that apply to your architecture. These are non-negotiable boundaries.*
|
|
2972
|
+
|
|
2973
|
+
### Technical Constraints
|
|
2974
|
+
|
|
2975
|
+
| Constraint | Background |
|
|
2976
|
+
|------------|------------|
|
|
2977
|
+
| *[e.g. Must run on Kubernetes]* | *[reason]* |
|
|
2978
|
+
| *[e.g. Postgres only \u2014 no other DBs]* | *[reason]* |
|
|
2979
|
+
|
|
2980
|
+
### Organizational Constraints
|
|
2981
|
+
|
|
2982
|
+
| Constraint | Background |
|
|
2983
|
+
|------------|------------|
|
|
2984
|
+
| *[e.g. Team of N developers]* | *[reason]* |
|
|
2985
|
+
| *[e.g. 2-week sprints]* | *[reason]* |
|
|
2986
|
+
|
|
2987
|
+
### Conventions
|
|
2988
|
+
|
|
2989
|
+
| Convention | Background |
|
|
2990
|
+
|------------|------------|
|
|
2991
|
+
| *[e.g. TypeScript strict mode]* | *[reason]* |
|
|
2992
|
+
`;
|
|
2993
|
+
}
|
|
2994
|
+
function buildSection6() {
|
|
2995
|
+
return `## 6. Runtime View
|
|
2996
|
+
|
|
2997
|
+
*Describe the important runtime scenarios \u2014 how the system behaves at runtime for key use cases.*
|
|
2998
|
+
|
|
2999
|
+
### Scenario 1: [Name the scenario]
|
|
3000
|
+
|
|
3001
|
+
\`\`\`
|
|
3002
|
+
[Actor] \u2192 [Component A] \u2192 [Component B] \u2192 [External System]
|
|
3003
|
+
\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
|
|
3004
|
+
\`\`\`
|
|
3005
|
+
|
|
3006
|
+
**Description:** *[Describe what happens step by step.]*
|
|
3007
|
+
|
|
3008
|
+
### Scenario 2: [Name the scenario]
|
|
3009
|
+
|
|
3010
|
+
*[Add more scenarios as needed.]*
|
|
3011
|
+
`;
|
|
3012
|
+
}
|
|
3013
|
+
function buildSection7() {
|
|
3014
|
+
return `## 7. Deployment View
|
|
3015
|
+
|
|
3016
|
+
*Describe the technical infrastructure \u2014 environments, nodes, and how the system is distributed.*
|
|
3017
|
+
|
|
3018
|
+
### Infrastructure Level 1
|
|
3019
|
+
|
|
3020
|
+
\`\`\`
|
|
3021
|
+
[Environment: Production]
|
|
3022
|
+
\u2514\u2500\u2500 [Server / Cloud Region]
|
|
3023
|
+
\u251C\u2500\u2500 [Application Tier]
|
|
3024
|
+
\u251C\u2500\u2500 [Database Tier]
|
|
3025
|
+
\u2514\u2500\u2500 [External Services]
|
|
3026
|
+
\`\`\`
|
|
3027
|
+
|
|
3028
|
+
### Environments
|
|
3029
|
+
|
|
3030
|
+
| Environment | Purpose | URL / Access |
|
|
3031
|
+
|-------------|---------|-------------|
|
|
3032
|
+
| Production | Live system | *[URL]* |
|
|
3033
|
+
| Staging | Pre-production testing | *[URL]* |
|
|
3034
|
+
| Development | Local dev | localhost |
|
|
3035
|
+
|
|
3036
|
+
*Fill in your actual infrastructure topology.*
|
|
3037
|
+
`;
|
|
3038
|
+
}
|
|
3039
|
+
function buildLlmContext(opts) {
|
|
3040
|
+
const { projectName, projectDescription, teamSize, techStack, digestContext: d, amberContext } = opts;
|
|
3041
|
+
const lines = [
|
|
3042
|
+
`Project: ${projectName}`,
|
|
3043
|
+
projectDescription ? `Description: ${projectDescription}` : "",
|
|
3044
|
+
teamSize ? `Team size: ${teamSize}` : "",
|
|
3045
|
+
techStack && techStack.length > 0 ? `Tech stack: ${techStack.join(", ")}` : "",
|
|
3046
|
+
``,
|
|
3047
|
+
`Architecture health: ${d.healthSummary}`,
|
|
3048
|
+
d.grade ? `Grade: ${d.grade}` : "",
|
|
3049
|
+
`Period: ${d.period}`,
|
|
3050
|
+
`Commits: ${d.commitCount}`,
|
|
3051
|
+
`Files changed: ${d.filesChanged}`,
|
|
3052
|
+
d.topCommits.length > 0 ? `Recent commits: ${d.topCommits.slice(0, 4).join("; ")}` : ""
|
|
3053
|
+
].filter(Boolean);
|
|
3054
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
3055
|
+
lines.push(
|
|
3056
|
+
``,
|
|
3057
|
+
`Capabilities: ${amberContext.capabilities.map((c) => c.name).join(", ")}`,
|
|
3058
|
+
`Total capabilities: ${amberContext.capabilities.length}`
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
return lines.join("\n");
|
|
3062
|
+
}
|
|
3063
|
+
async function generateLlmSections(opts) {
|
|
3064
|
+
const context = buildLlmContext(opts);
|
|
3065
|
+
const systemPrompt = `You are an expert software architect generating arc42 architecture documentation.
|
|
3066
|
+
Write concise, informative Markdown content for each section.
|
|
3067
|
+
Base your content on the provided PRISM architecture data and project information.
|
|
3068
|
+
Use professional, clear language appropriate for technical documentation.
|
|
3069
|
+
Do not reproduce copyrighted content. All insights are derived from the provided data.`;
|
|
3070
|
+
const userPrompt = `Generate four arc42 documentation sections for this project.
|
|
3071
|
+
|
|
3072
|
+
Project data:
|
|
3073
|
+
${context}
|
|
3074
|
+
|
|
3075
|
+
Generate the following four sections in Markdown. Each section should start with the exact header shown.
|
|
3076
|
+
Keep each section focused and practical \u2014 100\u2013300 words per section.
|
|
3077
|
+
|
|
3078
|
+
---
|
|
3079
|
+
## 1. Introduction and Goals
|
|
3080
|
+
|
|
3081
|
+
Write an overview of:
|
|
3082
|
+
- What the system does (based on the project name, description, and capabilities)
|
|
3083
|
+
- Key quality goals (2\u20133 measurable goals)
|
|
3084
|
+
- Key stakeholders and their expectations
|
|
3085
|
+
|
|
3086
|
+
---
|
|
3087
|
+
## 3. Context and Scope
|
|
3088
|
+
|
|
3089
|
+
Write:
|
|
3090
|
+
- A system context description (what the system does, what it connects to)
|
|
3091
|
+
- External systems and actors that interact with the system
|
|
3092
|
+
- What is explicitly out of scope
|
|
3093
|
+
|
|
3094
|
+
---
|
|
3095
|
+
## 4. Solution Strategy
|
|
3096
|
+
|
|
3097
|
+
Write:
|
|
3098
|
+
- Core technology decisions and why
|
|
3099
|
+
- Key architectural patterns used (infer from the tech stack and capability structure)
|
|
3100
|
+
- How quality goals are addressed by the architecture
|
|
3101
|
+
|
|
3102
|
+
---
|
|
3103
|
+
## 8. Crosscutting Concepts
|
|
3104
|
+
|
|
3105
|
+
Identify crosscutting concerns from the capability names and architecture:
|
|
3106
|
+
- Security / authentication approach
|
|
3107
|
+
- Error handling and logging
|
|
3108
|
+
- Caching strategy (if applicable)
|
|
3109
|
+
- Testing approach
|
|
3110
|
+
- Any other crosscutting patterns visible from the capability structure
|
|
3111
|
+
`;
|
|
3112
|
+
const response = await opts.llm.complete({
|
|
3113
|
+
systemPrompt,
|
|
3114
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
3115
|
+
maxTokens: 4096
|
|
3116
|
+
});
|
|
3117
|
+
const text = response.content;
|
|
3118
|
+
function extractSection(marker, nextMarker) {
|
|
3119
|
+
const start = text.indexOf(marker);
|
|
3120
|
+
if (start === -1) return "";
|
|
3121
|
+
const end = nextMarker ? text.indexOf(nextMarker, start + marker.length) : text.length;
|
|
3122
|
+
return end === -1 ? text.slice(start).trim() : text.slice(start, end).trim();
|
|
3123
|
+
}
|
|
3124
|
+
const s1 = extractSection("## 1. Introduction", "## 3. Context");
|
|
3125
|
+
const s3 = extractSection("## 3. Context", "## 4. Solution");
|
|
3126
|
+
const s4 = extractSection("## 4. Solution", "## 8. Crosscutting");
|
|
3127
|
+
const s8 = extractSection("## 8. Crosscutting", "");
|
|
3128
|
+
return { s1, s3, s4, s8 };
|
|
3129
|
+
}
|
|
3130
|
+
function fallbackSection1(opts) {
|
|
3131
|
+
const { projectName, projectDescription, amberContext } = opts;
|
|
3132
|
+
const capCount = amberContext?.capabilities.length ?? 0;
|
|
3133
|
+
return `## 1. Introduction and Goals
|
|
3134
|
+
|
|
3135
|
+
**${projectName}** ${projectDescription ?? "is a software system documented using arc42."}
|
|
3136
|
+
|
|
3137
|
+
### Quality Goals
|
|
3138
|
+
|
|
3139
|
+
| Priority | Quality Goal | Scenario |
|
|
3140
|
+
|----------|-------------|----------|
|
|
3141
|
+
| 1 | Correctness | System produces correct results |
|
|
3142
|
+
| 2 | Maintainability | New features can be added without regression |
|
|
3143
|
+
| 3 | Performance | System responds within acceptable time |
|
|
3144
|
+
|
|
3145
|
+
### Stakeholders
|
|
3146
|
+
|
|
3147
|
+
| Role | Expectations |
|
|
3148
|
+
|------|-------------|
|
|
3149
|
+
| Development Team | Clear architecture structure, ${capCount} documented capabilities |
|
|
3150
|
+
| Product Owner | Feature delivery aligned with roadmap |
|
|
3151
|
+
| Operations | System is deployable and observable |
|
|
3152
|
+
|
|
3153
|
+
*Generated from PRISM data \u2014 enrich with project-specific goals.*
|
|
3154
|
+
`;
|
|
3155
|
+
}
|
|
3156
|
+
function fallbackSection3(opts) {
|
|
3157
|
+
const { projectName } = opts;
|
|
3158
|
+
return `## 3. Context and Scope
|
|
3159
|
+
|
|
3160
|
+
### System Context
|
|
3161
|
+
|
|
3162
|
+
**${projectName}** interacts with the following external systems:
|
|
3163
|
+
|
|
3164
|
+
| External System | Description | Relationship |
|
|
3165
|
+
|----------------|-------------|-------------|
|
|
3166
|
+
| *[Users / Clients]* | End users of the system | Consumer |
|
|
3167
|
+
| *[External API / Service]* | *[Describe]* | Integration |
|
|
3168
|
+
| *[Database]* | Data persistence | Provider |
|
|
3169
|
+
|
|
3170
|
+
### In Scope
|
|
3171
|
+
|
|
3172
|
+
*[Describe what the system is responsible for.]*
|
|
3173
|
+
|
|
3174
|
+
### Out of Scope
|
|
3175
|
+
|
|
3176
|
+
*[Describe what is deliberately excluded.]*
|
|
3177
|
+
`;
|
|
3178
|
+
}
|
|
3179
|
+
function fallbackSection4(opts) {
|
|
3180
|
+
const { techStack } = opts;
|
|
3181
|
+
const stack = techStack && techStack.length > 0 ? techStack.join(", ") : "TypeScript, Next.js";
|
|
3182
|
+
return `## 4. Solution Strategy
|
|
3183
|
+
|
|
3184
|
+
### Technology Decisions
|
|
3185
|
+
|
|
3186
|
+
**Tech stack:** ${stack}
|
|
3187
|
+
|
|
3188
|
+
| Decision | Rationale |
|
|
3189
|
+
|----------|-----------|
|
|
3190
|
+
| *[Framework choice]* | *[Why this framework]* |
|
|
3191
|
+
| *[Database choice]* | *[Why this DB]* |
|
|
3192
|
+
| *[Architecture pattern]* | *[Why this pattern]* |
|
|
3193
|
+
|
|
3194
|
+
### Architectural Approach
|
|
3195
|
+
|
|
3196
|
+
*Describe the core architectural approach \u2014 e.g. monolith, microservices, event-driven, CQRS.*
|
|
3197
|
+
|
|
3198
|
+
### Quality Goal Mapping
|
|
3199
|
+
|
|
3200
|
+
| Quality Goal | Architecture Response |
|
|
3201
|
+
|-------------|----------------------|
|
|
3202
|
+
| Correctness | *[How architecture ensures correctness]* |
|
|
3203
|
+
| Maintainability | *[How architecture supports maintainability]* |
|
|
3204
|
+
`;
|
|
3205
|
+
}
|
|
3206
|
+
function fallbackSection8(opts) {
|
|
3207
|
+
return `## 8. Crosscutting Concepts
|
|
3208
|
+
|
|
3209
|
+
### Security
|
|
3210
|
+
|
|
3211
|
+
*Describe the authentication and authorization approach.*
|
|
3212
|
+
|
|
3213
|
+
### Error Handling
|
|
3214
|
+
|
|
3215
|
+
*Describe the error handling and logging strategy.*
|
|
3216
|
+
|
|
3217
|
+
### Observability
|
|
3218
|
+
|
|
3219
|
+
*Describe how the system is monitored \u2014 logs, metrics, tracing.*
|
|
3220
|
+
|
|
3221
|
+
### Testing Strategy
|
|
3222
|
+
|
|
3223
|
+
*Describe the testing levels \u2014 unit, integration, end-to-end.*
|
|
3224
|
+
|
|
3225
|
+
### Configuration
|
|
3226
|
+
|
|
3227
|
+
*Describe how the system is configured across environments.*
|
|
3228
|
+
`;
|
|
3229
|
+
}
|
|
3230
|
+
async function generateArc42(opts) {
|
|
3231
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3232
|
+
const s5Content = buildSection5(opts);
|
|
3233
|
+
const s9Content = buildSection9(opts);
|
|
3234
|
+
const s10Content = buildSection10(opts);
|
|
3235
|
+
const s11Content = buildSection11(opts);
|
|
3236
|
+
const s12Content = buildSection12(opts);
|
|
3237
|
+
const s2Content = buildSection2();
|
|
3238
|
+
const s6Content = buildSection6();
|
|
3239
|
+
const s7Content = buildSection7();
|
|
3240
|
+
let s1Content;
|
|
3241
|
+
let s3Content;
|
|
3242
|
+
let s4Content;
|
|
3243
|
+
let s8Content;
|
|
3244
|
+
try {
|
|
3245
|
+
const llmSections = await generateLlmSections(opts);
|
|
3246
|
+
s1Content = llmSections.s1 || fallbackSection1(opts);
|
|
3247
|
+
s3Content = llmSections.s3 || fallbackSection3(opts);
|
|
3248
|
+
s4Content = llmSections.s4 || fallbackSection4(opts);
|
|
3249
|
+
s8Content = llmSections.s8 || fallbackSection8(opts);
|
|
3250
|
+
} catch {
|
|
3251
|
+
s1Content = fallbackSection1(opts);
|
|
3252
|
+
s3Content = fallbackSection3(opts);
|
|
3253
|
+
s4Content = fallbackSection4(opts);
|
|
3254
|
+
s8Content = fallbackSection8();
|
|
3255
|
+
}
|
|
3256
|
+
const sections = [
|
|
3257
|
+
{ number: 1, title: "Introduction and Goals", content: s1Content, dataSource: "llm" },
|
|
3258
|
+
{ number: 2, title: "Constraints", content: s2Content, dataSource: "template" },
|
|
3259
|
+
{ number: 3, title: "Context and Scope", content: s3Content, dataSource: "llm" },
|
|
3260
|
+
{ number: 4, title: "Solution Strategy", content: s4Content, dataSource: "llm" },
|
|
3261
|
+
{ number: 5, title: "Building Block View", content: s5Content, dataSource: "prism" },
|
|
3262
|
+
{ number: 6, title: "Runtime View", content: s6Content, dataSource: "template" },
|
|
3263
|
+
{ number: 7, title: "Deployment View", content: s7Content, dataSource: "template" },
|
|
3264
|
+
{ number: 8, title: "Crosscutting Concepts", content: s8Content, dataSource: "llm" },
|
|
3265
|
+
{ number: 9, title: "Architecture Decisions", content: s9Content, dataSource: "prism" },
|
|
3266
|
+
{ number: 10, title: "Quality Requirements", content: s10Content, dataSource: "prism" },
|
|
3267
|
+
{ number: 11, title: "Risks and Technical Debt", content: s11Content, dataSource: "prism" },
|
|
3268
|
+
{ number: 12, title: "Glossary", content: s12Content, dataSource: "prism" }
|
|
3269
|
+
];
|
|
3270
|
+
const fullMarkdown = [
|
|
3271
|
+
`<!-- This document uses the arc42 template \xA9 arc42.org, licensed under CC-BY 4.0 -->`,
|
|
3272
|
+
`<!-- Generated by PRISM0x2A + forge0x2B \u2014 Architecture intelligence -->`,
|
|
3273
|
+
`<!-- ${generatedAt} -->`,
|
|
3274
|
+
``,
|
|
3275
|
+
`# Architecture Documentation \u2014 ${opts.projectName}`,
|
|
3276
|
+
``,
|
|
3277
|
+
`> ${ARC42_ATTRIBUTION}`,
|
|
3278
|
+
``,
|
|
3279
|
+
...sections.map((s) => s.content)
|
|
3280
|
+
].join("\n\n");
|
|
3281
|
+
return {
|
|
3282
|
+
sections,
|
|
3283
|
+
fullMarkdown,
|
|
3284
|
+
attribution: ARC42_ATTRIBUTION,
|
|
3285
|
+
generatedAt
|
|
3286
|
+
};
|
|
3287
|
+
}
|
|
3288
|
+
|
|
3289
|
+
// src/generators/generateKnowledgeCapture.ts
|
|
3290
|
+
function buildContext5(opts) {
|
|
3291
|
+
const { digestContext: d, amberContext, projectName, departingDeveloper, focusCapabilities } = opts;
|
|
3292
|
+
const lines = [
|
|
3293
|
+
`## Knowledge Capture Context`,
|
|
3294
|
+
`Project: ${projectName}`,
|
|
3295
|
+
departingDeveloper ? `Departing Developer: ${departingDeveloper}` : `Departing Developer: (not specified \u2014 write for general knowledge transfer)`,
|
|
3296
|
+
focusCapabilities && focusCapabilities.length > 0 ? `Focus Capabilities: ${focusCapabilities.join(", ")}` : `Focus Capabilities: all`,
|
|
3297
|
+
``,
|
|
3298
|
+
`## Recent Activity \u2014 ${d.period}`,
|
|
3299
|
+
`Commits: ${d.commitCount}`,
|
|
3300
|
+
`Files changed: ${d.filesChanged}`,
|
|
3301
|
+
`Architecture grade: ${d.grade ?? "N/A"}`,
|
|
3302
|
+
`Health: ${d.healthSummary}`,
|
|
3303
|
+
``
|
|
3304
|
+
];
|
|
3305
|
+
if (d.topCommits.length > 0) {
|
|
3306
|
+
lines.push(`### Recent Commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
|
|
3307
|
+
}
|
|
3308
|
+
if (d.newDrifts.length > 0) {
|
|
3309
|
+
lines.push(
|
|
3310
|
+
`### Drifted Capabilities (need documentation attention)`,
|
|
3311
|
+
...d.newDrifts.map((c) => `- ${c}`),
|
|
3312
|
+
``
|
|
3313
|
+
);
|
|
3314
|
+
}
|
|
3315
|
+
if (d.newRisks.length > 0) {
|
|
3316
|
+
lines.push(`### Known Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
3317
|
+
}
|
|
3318
|
+
if (amberContext) {
|
|
3319
|
+
lines.push(`## AMBER Capability Registry`);
|
|
3320
|
+
lines.push(`Total capabilities: ${amberContext.capabilities.length}`);
|
|
3321
|
+
lines.push(`Drifted: ${amberContext.driftedCapabilities}`);
|
|
3322
|
+
lines.push(`Tagged files: ${amberContext.taggedFiles} / ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`);
|
|
3323
|
+
lines.push(``);
|
|
3324
|
+
const toInclude = focusCapabilities && focusCapabilities.length > 0 ? amberContext.capabilities.filter(
|
|
3325
|
+
(c) => focusCapabilities.some((f) => c.name.toLowerCase().includes(f.toLowerCase()) || c.id.toLowerCase().includes(f.toLowerCase()))
|
|
3326
|
+
) : amberContext.capabilities;
|
|
3327
|
+
if (toInclude.length > 0) {
|
|
3328
|
+
lines.push(`### Capabilities`);
|
|
3329
|
+
for (const cap of toInclude.slice(0, 20)) {
|
|
3330
|
+
lines.push(`#### ${cap.name} (${cap.id})`);
|
|
3331
|
+
lines.push(`- Criticality: ${cap.criticality}`);
|
|
3332
|
+
lines.push(`- Lifecycle: ${cap.lifecycle}`);
|
|
3333
|
+
lines.push(`- Files: ${cap.files.length}`);
|
|
3334
|
+
lines.push(`- Drift: ${cap.driftCount} file${cap.driftCount !== 1 ? "s" : ""} out of date`);
|
|
3335
|
+
if (cap.description) lines.push(`- Description: ${cap.description}`);
|
|
3336
|
+
lines.push(``);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
if (amberContext.orphanedFiles.length > 0) {
|
|
3340
|
+
lines.push(
|
|
3341
|
+
`### Orphaned Files (no capability tag)`,
|
|
3342
|
+
...amberContext.orphanedFiles.slice(0, 10).map((f) => `- ${f}`),
|
|
3343
|
+
``
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
return lines.join("\n");
|
|
3348
|
+
}
|
|
3349
|
+
function buildSystemPrompt13(opts) {
|
|
3350
|
+
const devName = opts.departingDeveloper ?? "a departing developer";
|
|
3351
|
+
return [
|
|
3352
|
+
`You are generating a Knowledge Capture document \u2014 a complete architectural brain dump before ${devName} leaves the team.`,
|
|
3353
|
+
`This document is crucial. It preserves institutional knowledge that would otherwise walk out the door.`,
|
|
3354
|
+
`Write with warmth and humanity \u2014 this is someone's legacy. Be thorough, specific, and genuinely useful.`,
|
|
3355
|
+
`Make it feel like ${devName} wrote it themselves, with care for the next person who will maintain this system.`,
|
|
3356
|
+
`Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
|
|
3357
|
+
].join(" ");
|
|
3358
|
+
}
|
|
3359
|
+
function buildUserPrompt13(opts) {
|
|
3360
|
+
const context = buildContext5(opts);
|
|
3361
|
+
const devName = opts.departingDeveloper ?? "the departing developer";
|
|
3362
|
+
return [
|
|
3363
|
+
`Generate a complete Knowledge Capture document for: ${opts.projectName}`,
|
|
3364
|
+
`This preserves ${devName}'s architectural knowledge for the team.`,
|
|
3365
|
+
``,
|
|
3366
|
+
context,
|
|
3367
|
+
``,
|
|
3368
|
+
`## Required Document Structure`,
|
|
3369
|
+
`The document must cover these 6 sections:`,
|
|
3370
|
+
`1. Architecture Overview \u2014 what the system does, how capabilities relate to each other, the mental model`,
|
|
3371
|
+
`2. Critical Capabilities \u2014 deep dive on the most important/drifted/complex capabilities, what makes them tricky`,
|
|
3372
|
+
`3. Known Issues & Workarounds \u2014 things that don't work as expected, temporary hacks, "do not touch" zones`,
|
|
3373
|
+
`4. Where to Start \u2014 recommended reading order for a new developer, which files to read first`,
|
|
3374
|
+
`5. Tribal Knowledge \u2014 things not obvious from code: why decisions were made, what was tried and failed, hidden dependencies`,
|
|
3375
|
+
`6. Onboarding Checklist \u2014 concrete 10-step list for a new team member joining this project`,
|
|
3376
|
+
``,
|
|
3377
|
+
`## Output Format`,
|
|
3378
|
+
`Return a single JSON object with this exact shape:`,
|
|
3379
|
+
`{`,
|
|
3380
|
+
` "sections": [`,
|
|
3381
|
+
` { "title": "<section title>", "content": "<full markdown content for this section, multiple paragraphs>" }`,
|
|
3382
|
+
` ],`,
|
|
3383
|
+
` "criticalKnowledge": ["<most important thing 1>", "<most important thing 2>", ...],`,
|
|
3384
|
+
` "onboardingChecklist": ["<step 1: specific action>", "<step 2: specific action>", ...]`,
|
|
3385
|
+
`}`,
|
|
3386
|
+
``,
|
|
3387
|
+
`Rules:`,
|
|
3388
|
+
`- sections: exactly 6, in the order above, with rich content (not just bullets)`,
|
|
3389
|
+
`- criticalKnowledge: 5-8 bullets \u2014 the things a new developer MUST know or they will make mistakes`,
|
|
3390
|
+
`- onboardingChecklist: exactly 10 items \u2014 concrete, actionable steps (not "read the README", but "read src/auth/ starting with session.ts")`,
|
|
3391
|
+
`- Write as if ${devName} is speaking directly to their replacement \u2014 warm, honest, specific`,
|
|
3392
|
+
`- Include file paths and capability names from the context where relevant`,
|
|
3393
|
+
`- Derive all content from the context \u2014 no invented facts`,
|
|
3394
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
3395
|
+
].join("\n");
|
|
3396
|
+
}
|
|
3397
|
+
function sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist) {
|
|
3398
|
+
const devName = opts.departingDeveloper ?? "the team";
|
|
3399
|
+
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
3400
|
+
const parts = [
|
|
3401
|
+
`# Knowledge Capture: ${opts.projectName}`,
|
|
3402
|
+
``,
|
|
3403
|
+
`> **Prepared by:** ${devName} `,
|
|
3404
|
+
`> **Date:** ${date} `,
|
|
3405
|
+
`> **Purpose:** Preserve architectural knowledge for the next maintainer`,
|
|
3406
|
+
``,
|
|
3407
|
+
`---`,
|
|
3408
|
+
``,
|
|
3409
|
+
`## TL;DR \u2014 Critical Knowledge`,
|
|
3410
|
+
``,
|
|
3411
|
+
...criticalKnowledge.map((k) => `- ${k}`),
|
|
3412
|
+
``,
|
|
3413
|
+
`---`,
|
|
3414
|
+
``
|
|
3415
|
+
];
|
|
3416
|
+
for (const section of sections) {
|
|
3417
|
+
parts.push(`## ${section.title}`, ``, section.content, ``, `---`, ``);
|
|
3418
|
+
}
|
|
3419
|
+
parts.push(
|
|
3420
|
+
`## Onboarding Checklist`,
|
|
3421
|
+
``,
|
|
3422
|
+
`For the next person joining this project:`,
|
|
3423
|
+
``,
|
|
3424
|
+
...onboardingChecklist.map((step, i) => `- [ ] **Step ${i + 1}:** ${step}`),
|
|
3425
|
+
``,
|
|
3426
|
+
`---`,
|
|
3427
|
+
``,
|
|
3428
|
+
`*Generated with [forge0x2B](https://forge0x2b.dev) \xB7 Architecture intelligence for engineering teams*`
|
|
3429
|
+
);
|
|
3430
|
+
return parts.join("\n");
|
|
3431
|
+
}
|
|
3432
|
+
function buildFallback7(opts) {
|
|
3433
|
+
const { digestContext: d, amberContext, projectName, departingDeveloper } = opts;
|
|
3434
|
+
const critCaps = amberContext?.capabilities.filter((c) => c.criticality === "critical" || c.driftCount > 0).slice(0, 5).map((c) => c.name) ?? [];
|
|
3435
|
+
const sections = [
|
|
3436
|
+
{
|
|
3437
|
+
title: "Architecture Overview",
|
|
3438
|
+
content: `${projectName} is composed of ${amberContext?.capabilities.length ?? "several"} tracked capabilities. ${d.healthSummary}
|
|
3439
|
+
|
|
3440
|
+
The system currently has ${d.filesChanged} recently changed files and a coherence grade of ${d.grade ?? "unknown"}.`
|
|
3441
|
+
},
|
|
3442
|
+
{
|
|
3443
|
+
title: "Critical Capabilities",
|
|
3444
|
+
content: critCaps.length > 0 ? `The following capabilities require the most attention:
|
|
3445
|
+
|
|
3446
|
+
${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.`
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
title: "Known Issues & Workarounds",
|
|
3450
|
+
content: d.newRisks.length > 0 ? `The following risks have been identified:
|
|
3451
|
+
|
|
3452
|
+
${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.`
|
|
3453
|
+
},
|
|
3454
|
+
{
|
|
3455
|
+
title: "Where to Start",
|
|
3456
|
+
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.
|
|
3457
|
+
|
|
3458
|
+
Focus on capabilities with high drift counts first \u2014 those are where documentation has fallen behind the code.`
|
|
3459
|
+
},
|
|
3460
|
+
{
|
|
3461
|
+
title: "Tribal Knowledge",
|
|
3462
|
+
content: `Key things not obvious from reading the code:
|
|
3463
|
+
|
|
3464
|
+
- The AMBER tags in source files are the source of truth for capability ownership
|
|
3465
|
+
- Drift count indicates documentation debt \u2014 files changed without updating @amber-doc tags
|
|
3466
|
+
- Architecture score changes over time \u2014 check \`.green/\` for trend data`
|
|
3467
|
+
},
|
|
3468
|
+
{
|
|
3469
|
+
title: "Architecture Decisions",
|
|
3470
|
+
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.`
|
|
3471
|
+
}
|
|
3472
|
+
];
|
|
3473
|
+
const criticalKnowledge = [
|
|
3474
|
+
`The AMBER capability registry (.amber/) is the source of truth for how the codebase is organized`,
|
|
3475
|
+
`Drift count > 0 means documentation hasn't kept pace with code changes \u2014 fix before adding features`,
|
|
3476
|
+
amberContext ? `${amberContext.driftedCapabilities} of ${amberContext.capabilities.length} capabilities currently have drift` : `Check drift count before starting work`,
|
|
3477
|
+
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`,
|
|
3478
|
+
`Architecture grade: ${d.grade ?? "unknown"} \u2014 understand what's driving this before making broad changes`
|
|
3479
|
+
].filter(Boolean);
|
|
3480
|
+
const onboardingChecklist = [
|
|
3481
|
+
`Read this entire document before writing any code`,
|
|
3482
|
+
`Install and run PRISM to get a live architecture view`,
|
|
3483
|
+
`Review .amber/capabilities.md \u2014 understand every capability and its criticality`,
|
|
3484
|
+
`Check .amber/state.json to see which files belong to which capabilities`,
|
|
3485
|
+
`Run the test suite and confirm it passes before making changes`,
|
|
3486
|
+
`Read the top ${Math.min(d.topCommits.length || 5, 5)} recent commits to understand current momentum`,
|
|
3487
|
+
`Review capabilities with drift count > 0 \u2014 read both the code and the docs`,
|
|
3488
|
+
`Set up forge0x2B to get daily Architecture Radio briefings`,
|
|
3489
|
+
`Ask the team about any "do not touch" areas that aren't documented`,
|
|
3490
|
+
`Make your first PR small and well-scoped to verify your understanding`
|
|
3491
|
+
];
|
|
3492
|
+
const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
|
|
3493
|
+
return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
|
|
3494
|
+
}
|
|
3495
|
+
function parseLlmResponse7(raw, opts) {
|
|
3496
|
+
let text = raw.trim();
|
|
3497
|
+
if (text.startsWith("```")) {
|
|
3498
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
3499
|
+
}
|
|
3500
|
+
let parsed;
|
|
3501
|
+
try {
|
|
3502
|
+
parsed = JSON.parse(text);
|
|
3503
|
+
} catch {
|
|
3504
|
+
return null;
|
|
3505
|
+
}
|
|
3506
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
3507
|
+
const sections = rawSections.filter((s) => typeof s.title === "string").map((s) => {
|
|
3508
|
+
const item = s;
|
|
3509
|
+
return {
|
|
3510
|
+
title: item.title,
|
|
3511
|
+
content: typeof item.content === "string" ? item.content : ""
|
|
3512
|
+
};
|
|
3513
|
+
});
|
|
3514
|
+
if (sections.length === 0) return null;
|
|
3515
|
+
const rawCritical = Array.isArray(parsed.criticalKnowledge) ? parsed.criticalKnowledge : [];
|
|
3516
|
+
const criticalKnowledge = rawCritical.filter((k) => typeof k === "string").map((k) => k);
|
|
3517
|
+
const rawChecklist = Array.isArray(parsed.onboardingChecklist) ? parsed.onboardingChecklist : [];
|
|
3518
|
+
const onboardingChecklist = rawChecklist.filter((k) => typeof k === "string").map((k) => k);
|
|
3519
|
+
const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
|
|
3520
|
+
return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
|
|
3521
|
+
}
|
|
3522
|
+
async function generateKnowledgeCapture(options) {
|
|
3523
|
+
try {
|
|
3524
|
+
const response = await options.llm.complete({
|
|
3525
|
+
systemPrompt: buildSystemPrompt13(options),
|
|
3526
|
+
messages: [{ role: "user", content: buildUserPrompt13(options) }],
|
|
3527
|
+
maxTokens: 8192
|
|
3528
|
+
});
|
|
3529
|
+
const parsed = parseLlmResponse7(response.content, options);
|
|
3530
|
+
if (parsed) return parsed;
|
|
3531
|
+
} catch {
|
|
3532
|
+
}
|
|
3533
|
+
return buildFallback7(options);
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// src/generators/generateApiChangelog.ts
|
|
3537
|
+
function buildPromptContext(opts) {
|
|
3538
|
+
const { apiChangeContext: ctx, amberContext, gitContext } = opts;
|
|
3539
|
+
const lines = [
|
|
3540
|
+
`## Change Range`,
|
|
3541
|
+
`From: ${ctx.from} To: ${ctx.to}`,
|
|
3542
|
+
`Total API changes: ${ctx.totalChanged}`,
|
|
3543
|
+
``
|
|
3544
|
+
];
|
|
3545
|
+
if (ctx.breakingChanges.length > 0) {
|
|
3546
|
+
lines.push(`## Breaking Changes (${ctx.breakingChanges.length})`);
|
|
3547
|
+
for (const c of ctx.breakingChanges) {
|
|
3548
|
+
const capHint = c.capabilityName ? ` [${c.capabilityName}]` : "";
|
|
3549
|
+
lines.push(`- ${c.endpoint}${capHint} \u2014 ${c.description ?? "no description"} (file: ${c.file})`);
|
|
3550
|
+
}
|
|
3551
|
+
lines.push("");
|
|
3552
|
+
}
|
|
3553
|
+
if (ctx.newEndpoints.length > 0) {
|
|
3554
|
+
lines.push(`## New Endpoints (${ctx.newEndpoints.length})`);
|
|
3555
|
+
for (const c of ctx.newEndpoints) {
|
|
3556
|
+
const capHint = c.capabilityName ? ` [${c.capabilityName}]` : "";
|
|
3557
|
+
lines.push(`- ${c.endpoint}${capHint} \u2014 ${c.description ?? "new"} (file: ${c.file})`);
|
|
3558
|
+
}
|
|
3559
|
+
lines.push("");
|
|
3560
|
+
}
|
|
3561
|
+
if (ctx.removedEndpoints.length > 0) {
|
|
3562
|
+
lines.push(`## Removed Endpoints (${ctx.removedEndpoints.length})`);
|
|
3563
|
+
for (const c of ctx.removedEndpoints) {
|
|
3564
|
+
lines.push(`- ${c.endpoint} \u2014 removed from ${c.file}`);
|
|
3565
|
+
}
|
|
3566
|
+
lines.push("");
|
|
3567
|
+
}
|
|
3568
|
+
const modified = ctx.changes.filter((c) => c.changeType === "modified");
|
|
3569
|
+
if (modified.length > 0) {
|
|
3570
|
+
lines.push(`## Modified Endpoints (${modified.length})`);
|
|
3571
|
+
for (const c of modified) {
|
|
3572
|
+
const capHint = c.capabilityName ? ` [${c.capabilityName}]` : "";
|
|
3573
|
+
lines.push(`- ${c.endpoint}${capHint} \u2014 ${c.description ?? "modified"} (file: ${c.file})`);
|
|
3574
|
+
}
|
|
3575
|
+
lines.push("");
|
|
3576
|
+
}
|
|
3577
|
+
if (amberContext) {
|
|
3578
|
+
lines.push(`## AMBER Capability Context`, amberContext.summary, "");
|
|
3579
|
+
}
|
|
3580
|
+
if (gitContext && gitContext.commitMessages.length > 0) {
|
|
3581
|
+
lines.push(
|
|
3582
|
+
`## Recent Commits`,
|
|
3583
|
+
...gitContext.commitMessages.slice(0, 8).map((m) => `- ${m}`),
|
|
3584
|
+
""
|
|
3585
|
+
);
|
|
3586
|
+
}
|
|
3587
|
+
return lines.join("\n");
|
|
3588
|
+
}
|
|
3589
|
+
function buildSystemPrompt14(audience, format) {
|
|
3590
|
+
const audienceDesc = audience === "external" ? "external API consumers (third-party developers)" : audience === "partner" ? "integration partners with existing API contracts" : "internal engineering teams";
|
|
3591
|
+
const formatDesc = format === "migration-guide" ? "a step-by-step migration guide with code examples" : format === "release-notes" ? "polished release notes suitable for a public changelog page" : "a structured API changelog";
|
|
3592
|
+
return `You are a developer relations engineer writing ${formatDesc} for ${audienceDesc}. Be precise about endpoint paths, HTTP methods, and request/response changes. For breaking changes, always include a migration path. Generate ONLY valid JSON \u2014 no markdown fences, no prose outside the JSON object.`;
|
|
3593
|
+
}
|
|
3594
|
+
function buildUserPrompt14(opts, context) {
|
|
3595
|
+
const { projectName, version, targetAudience, format } = opts;
|
|
3596
|
+
const v = version || opts.apiChangeContext.to;
|
|
3597
|
+
return [
|
|
3598
|
+
`Generate an API ${format} for: ${projectName}`,
|
|
3599
|
+
`Version: ${v}`,
|
|
3600
|
+
`Target audience: ${targetAudience}`,
|
|
3601
|
+
`Format: ${format}`,
|
|
3602
|
+
``,
|
|
3603
|
+
context,
|
|
3604
|
+
``,
|
|
3605
|
+
`## Output Format`,
|
|
3606
|
+
`Return a single JSON object with this EXACT shape:`,
|
|
3607
|
+
`{`,
|
|
3608
|
+
` "version": "<version string>",`,
|
|
3609
|
+
` "date": "<ISO date YYYY-MM-DD>",`,
|
|
3610
|
+
` "targetAudience": "<audience>",`,
|
|
3611
|
+
` "breakingChanges": [`,
|
|
3612
|
+
` { "endpoint": "<METHOD /path>", "description": "<what changed>", "migration": "<how to migrate>" }`,
|
|
3613
|
+
` ],`,
|
|
3614
|
+
` "newFeatures": [`,
|
|
3615
|
+
` { "endpoint": "<METHOD /path>", "description": "<what it does>", "example": "<optional curl/code hint>" }`,
|
|
3616
|
+
` ],`,
|
|
3617
|
+
` "improvements": [`,
|
|
3618
|
+
` { "endpoint": "<METHOD /path>", "description": "<what improved>" }`,
|
|
3619
|
+
` ],`,
|
|
3620
|
+
` "deprecations": [`,
|
|
3621
|
+
` { "endpoint": "<METHOD /path>", "description": "<what is deprecated>", "sunset": "<optional date>" }`,
|
|
3622
|
+
` ],`,
|
|
3623
|
+
` "migrationGuide": "<full migration guidance as prose \u2014 2\u20135 paragraphs>",`,
|
|
3624
|
+
` "consumerImpactSummary": "<1 paragraph copy-ready summary for consumers>"`,
|
|
3625
|
+
`}`,
|
|
3626
|
+
``,
|
|
3627
|
+
`Rules:`,
|
|
3628
|
+
`- Every breaking change MUST have a non-empty migration string.`,
|
|
3629
|
+
`- For external/partner audiences, omit internal implementation details.`,
|
|
3630
|
+
`- migrationGuide should be empty string if no breaking changes.`,
|
|
3631
|
+
`- Return ONLY valid JSON. No markdown fences.`
|
|
3632
|
+
].join("\n");
|
|
3633
|
+
}
|
|
3634
|
+
function buildFallback8(opts) {
|
|
3635
|
+
const ctx = opts.apiChangeContext;
|
|
3636
|
+
const v = opts.version ?? ctx.to;
|
|
3637
|
+
return {
|
|
3638
|
+
version: v,
|
|
3639
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3640
|
+
targetAudience: opts.targetAudience,
|
|
3641
|
+
breakingChanges: ctx.breakingChanges.map((c) => ({
|
|
3642
|
+
endpoint: c.endpoint,
|
|
3643
|
+
description: c.description ?? "Breaking change detected",
|
|
3644
|
+
migration: "Review the updated API contract and update your client accordingly."
|
|
3645
|
+
})),
|
|
3646
|
+
newFeatures: ctx.newEndpoints.map((c) => ({
|
|
3647
|
+
endpoint: c.endpoint,
|
|
3648
|
+
description: c.description ?? "New endpoint added"
|
|
3649
|
+
})),
|
|
3650
|
+
improvements: ctx.changes.filter((c) => c.changeType === "modified").map((c) => ({
|
|
3651
|
+
endpoint: c.endpoint,
|
|
3652
|
+
description: c.description ?? "Endpoint updated"
|
|
3653
|
+
})),
|
|
3654
|
+
deprecations: ctx.removedEndpoints.map((c) => ({
|
|
3655
|
+
endpoint: c.endpoint,
|
|
3656
|
+
description: "This endpoint has been removed."
|
|
3657
|
+
})),
|
|
3658
|
+
migrationGuide: ctx.breakingChanges.length > 0 ? `This release contains ${ctx.breakingChanges.length} breaking change(s). Review the breaking changes section and update your API client before deploying. Test all affected endpoints in a staging environment.` : "",
|
|
3659
|
+
consumerImpactSummary: `Version ${v} includes ${ctx.newEndpoints.length} new endpoint(s), ${ctx.breakingChanges.length} breaking change(s), and ${ctx.changes.filter((c) => c.changeType === "modified").length} improvement(s). ` + (ctx.breakingChanges.length > 0 ? "Migration is required before upgrading." : "This release is backward-compatible.")
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function parseLlmResponse8(raw) {
|
|
3663
|
+
let text = raw.trim();
|
|
3664
|
+
if (text.startsWith("```")) {
|
|
3665
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
3666
|
+
}
|
|
3667
|
+
let parsed;
|
|
3668
|
+
try {
|
|
3669
|
+
parsed = JSON.parse(text);
|
|
3670
|
+
} catch {
|
|
3671
|
+
return null;
|
|
3672
|
+
}
|
|
3673
|
+
function parseChangeArray(key, required) {
|
|
3674
|
+
if (!Array.isArray(parsed[key])) return [];
|
|
3675
|
+
return parsed[key].filter((item) => {
|
|
3676
|
+
if (typeof item !== "object" || item === null) return false;
|
|
3677
|
+
return required.every((k) => k in item);
|
|
3678
|
+
});
|
|
3679
|
+
}
|
|
3680
|
+
return {
|
|
3681
|
+
version: typeof parsed.version === "string" ? parsed.version : "",
|
|
3682
|
+
date: typeof parsed.date === "string" ? parsed.date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3683
|
+
targetAudience: typeof parsed.targetAudience === "string" ? parsed.targetAudience : "",
|
|
3684
|
+
breakingChanges: parseChangeArray("breakingChanges", ["endpoint", "description", "migration"]),
|
|
3685
|
+
newFeatures: parseChangeArray("newFeatures", ["endpoint", "description"]),
|
|
3686
|
+
improvements: parseChangeArray("improvements", ["endpoint", "description"]),
|
|
3687
|
+
deprecations: parseChangeArray("deprecations", ["endpoint", "description"]),
|
|
3688
|
+
migrationGuide: typeof parsed.migrationGuide === "string" ? parsed.migrationGuide : "",
|
|
3689
|
+
consumerImpactSummary: typeof parsed.consumerImpactSummary === "string" ? parsed.consumerImpactSummary : ""
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
async function generateApiChangelog(opts) {
|
|
3693
|
+
const context = buildPromptContext(opts);
|
|
3694
|
+
try {
|
|
3695
|
+
const response = await opts.llm.complete({
|
|
3696
|
+
systemPrompt: buildSystemPrompt14(opts.targetAudience, opts.format),
|
|
3697
|
+
messages: [{ role: "user", content: buildUserPrompt14(opts, context) }],
|
|
3698
|
+
maxTokens: 3072
|
|
3699
|
+
});
|
|
3700
|
+
const parsed = parseLlmResponse8(response.content);
|
|
3701
|
+
if (parsed) return parsed;
|
|
3702
|
+
} catch {
|
|
3703
|
+
}
|
|
3704
|
+
return buildFallback8(opts);
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
// src/generators/generateChangeImpactBrief.ts
|
|
3708
|
+
function buildPromptContext2(opts) {
|
|
3709
|
+
const { gitContext, changeImpactContext: ctx, amberContext, greenContext } = opts;
|
|
3710
|
+
const lines = [
|
|
3711
|
+
`## Change Range`,
|
|
3712
|
+
`From: ${ctx.from}`,
|
|
3713
|
+
`To: ${ctx.to}`,
|
|
3714
|
+
`Commits: ${ctx.commitCount}`,
|
|
3715
|
+
`Files changed: ${ctx.filesChanged.length}`,
|
|
3716
|
+
``,
|
|
3717
|
+
`## Changed Files (sample)`,
|
|
3718
|
+
...ctx.filesChanged.slice(0, 20).map((f) => `- ${f}`)
|
|
3719
|
+
];
|
|
3720
|
+
if (ctx.breakingChanges.length > 0) {
|
|
3721
|
+
lines.push(``, `## Breaking Changes`, ...ctx.breakingChanges.map((b) => `- ${b}`));
|
|
3722
|
+
}
|
|
3723
|
+
if (ctx.affectedCapabilities.length > 0) {
|
|
3724
|
+
lines.push(``, `## Affected Business Capabilities`);
|
|
3725
|
+
for (const cap of ctx.affectedCapabilities) {
|
|
3726
|
+
lines.push(
|
|
3727
|
+
`- [${cap.criticality.toUpperCase()}] ${cap.name} (${cap.changedFiles.length} files changed, drift: ${cap.driftCount})`
|
|
3728
|
+
);
|
|
647
3729
|
}
|
|
648
|
-
stack.delete(node);
|
|
649
3730
|
}
|
|
650
|
-
|
|
651
|
-
|
|
3731
|
+
if (ctx.regulatoryCapabilities.length > 0) {
|
|
3732
|
+
lines.push(
|
|
3733
|
+
``,
|
|
3734
|
+
`## Critical Capabilities Touched (require approval)`,
|
|
3735
|
+
...ctx.regulatoryCapabilities.map((c) => `- ${c}`)
|
|
3736
|
+
);
|
|
652
3737
|
}
|
|
653
|
-
|
|
3738
|
+
if (ctx.coherenceScoreBefore !== null || ctx.coherenceScoreAfter !== null) {
|
|
3739
|
+
lines.push(
|
|
3740
|
+
``,
|
|
3741
|
+
`## Architecture Coherence`,
|
|
3742
|
+
`Score before: ${ctx.coherenceScoreBefore ?? "unknown"}`,
|
|
3743
|
+
`Score after: ${ctx.coherenceScoreAfter ?? "unknown"}`,
|
|
3744
|
+
`Delta: ${ctx.coherenceDelta !== null ? (ctx.coherenceDelta > 0 ? "+" : "") + ctx.coherenceDelta : "unknown"}`
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
if (ctx.topRisks.length > 0) {
|
|
3748
|
+
lines.push(``, `## Top Risks`, ...ctx.topRisks.map((r) => `- ${r}`));
|
|
3749
|
+
}
|
|
3750
|
+
if (amberContext) {
|
|
3751
|
+
lines.push(``, `## AMBER Layer Context`, amberContext.summary);
|
|
3752
|
+
}
|
|
3753
|
+
if (greenContext) {
|
|
3754
|
+
lines.push(``, `## GREEN Layer Context`, greenContext.summary);
|
|
3755
|
+
}
|
|
3756
|
+
if (gitContext.commitMessages.length > 0) {
|
|
3757
|
+
lines.push(
|
|
3758
|
+
``,
|
|
3759
|
+
`## Recent Commit Messages (sample)`,
|
|
3760
|
+
...gitContext.commitMessages.slice(0, 10).map((m) => `- ${m}`)
|
|
3761
|
+
);
|
|
3762
|
+
}
|
|
3763
|
+
return lines.join("\n");
|
|
654
3764
|
}
|
|
655
|
-
function
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
`## High-Coupling Hot Spots (high total connections)`,
|
|
672
|
-
...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
|
|
3765
|
+
function buildSystemPrompt15(audience) {
|
|
3766
|
+
if (audience === "executive") {
|
|
3767
|
+
return "You are a VP of Engineering preparing a release briefing for the CTO and board. Write in plain business language \u2014 no jargon, no technical acronyms. Be direct about risks. Generate ONLY valid JSON \u2014 no markdown fences, no prose outside the JSON.";
|
|
3768
|
+
}
|
|
3769
|
+
if (audience === "cab") {
|
|
3770
|
+
return "You are a senior engineering manager preparing a Change Advisory Board (CAB) submission. Balance technical precision with business impact. Highlight rollback complexity and compliance implications. Generate ONLY valid JSON \u2014 no markdown fences, no prose outside the JSON.";
|
|
3771
|
+
}
|
|
3772
|
+
return "You are a principal engineer preparing a technical change impact brief. Be precise about files, capabilities, and risk vectors. Generate ONLY valid JSON \u2014 no markdown fences, no prose outside the JSON.";
|
|
3773
|
+
}
|
|
3774
|
+
function buildUserPrompt15(opts, context) {
|
|
3775
|
+
const { projectName, releaseVersion, audience } = opts;
|
|
3776
|
+
const version = releaseVersion || opts.changeImpactContext.to;
|
|
3777
|
+
return [
|
|
3778
|
+
`Generate a change impact brief for: ${projectName}`,
|
|
3779
|
+
`Release version: ${version}`,
|
|
3780
|
+
`Audience: ${audience}`,
|
|
673
3781
|
``,
|
|
674
|
-
|
|
675
|
-
...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
|
|
3782
|
+
context,
|
|
676
3783
|
``,
|
|
677
|
-
`##
|
|
678
|
-
|
|
3784
|
+
`## Output Format`,
|
|
3785
|
+
`Return a single JSON object with this EXACT shape:`,
|
|
3786
|
+
`{`,
|
|
3787
|
+
` "title": "<brief title>",`,
|
|
3788
|
+
` "releaseVersion": "<version string>",`,
|
|
3789
|
+
` "date": "<ISO date>",`,
|
|
3790
|
+
` "audience": "<audience>",`,
|
|
3791
|
+
` "executiveSummary": "<3 bullet points separated by \\n, plain language, max 120 chars each>",`,
|
|
3792
|
+
` "technicalSummary": "<2-3 sentences for CAB technical reviewers>",`,
|
|
3793
|
+
` "affectedBusinessAreas": ["<area 1>", "<area 2>"],`,
|
|
3794
|
+
` "riskLevel": "<one of: low | medium | high | critical>",`,
|
|
3795
|
+
` "riskJustification": "<1-2 sentences explaining the risk rating>",`,
|
|
3796
|
+
` "complianceImpact": "<which regulated capabilities are touched, or 'None' if clean>",`,
|
|
3797
|
+
` "rollbackComplexity": "<one of: simple | moderate | complex>",`,
|
|
3798
|
+
` "rollbackGuidance": "<concise rollback instructions>",`,
|
|
3799
|
+
` "recommendedActions": ["<action 1>", "<action 2>", "<action 3>"],`,
|
|
3800
|
+
` "approvalRequired": <true | false>,`,
|
|
3801
|
+
` "metrics": {`,
|
|
3802
|
+
` "commits": <number>,`,
|
|
3803
|
+
` "filesChanged": <number>,`,
|
|
3804
|
+
` "capabilitiesAffected": <number>,`,
|
|
3805
|
+
` "coherenceDelta": <number | null>`,
|
|
3806
|
+
` }`,
|
|
3807
|
+
`}`,
|
|
679
3808
|
``,
|
|
680
|
-
`
|
|
681
|
-
|
|
682
|
-
|
|
3809
|
+
`Rules:`,
|
|
3810
|
+
`- riskLevel = "critical" if any critical capabilities touched OR breaking changes exist.`,
|
|
3811
|
+
`- riskLevel = "high" if 3+ capabilities affected or coherenceDelta < -5.`,
|
|
3812
|
+
`- approvalRequired = true if riskLevel is "critical" or "high".`,
|
|
3813
|
+
`- rollbackComplexity = "complex" if breaking changes or critical capabilities touched.`,
|
|
3814
|
+
`- executiveSummary must be EXACTLY 3 bullet lines joined by \\n \u2014 each starting with a dash.`,
|
|
3815
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON object.`
|
|
3816
|
+
].join("\n");
|
|
683
3817
|
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
3818
|
+
function buildFallback9(opts) {
|
|
3819
|
+
const ctx = opts.changeImpactContext;
|
|
3820
|
+
const hasCritical = ctx.regulatoryCapabilities.length > 0 || ctx.breakingChanges.length > 0;
|
|
3821
|
+
const hasHigh = ctx.affectedCapabilities.filter((c) => c.criticality === "high").length >= 2;
|
|
3822
|
+
const riskLevel = hasCritical ? "critical" : hasHigh || ctx.affectedCapabilities.length >= 3 ? "high" : ctx.affectedCapabilities.length >= 1 ? "medium" : "low";
|
|
3823
|
+
const approvalRequired = riskLevel === "critical" || riskLevel === "high";
|
|
3824
|
+
const rollbackComplexity = hasCritical ? "complex" : ctx.affectedCapabilities.length >= 3 ? "moderate" : "simple";
|
|
3825
|
+
return {
|
|
3826
|
+
title: `Change Impact Brief \u2014 ${opts.projectName}`,
|
|
3827
|
+
releaseVersion: opts.releaseVersion ?? ctx.to,
|
|
3828
|
+
date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3829
|
+
audience: opts.audience,
|
|
3830
|
+
executiveSummary: `- ${ctx.commitCount} commit(s) deployed across ${ctx.filesChanged.length} files
|
|
3831
|
+
- ${ctx.affectedCapabilities.length} business capability(ies) affected
|
|
3832
|
+
- Risk level: ${riskLevel.toUpperCase()}${hasCritical ? " \u2014 critical capabilities touched" : ""}`,
|
|
3833
|
+
technicalSummary: `${ctx.commitCount} commit(s) between ${ctx.from} and ${ctx.to}, changing ${ctx.filesChanged.length} file(s). ` + (ctx.breakingChanges.length > 0 ? `${ctx.breakingChanges.length} breaking change(s) detected. ` : "") + (ctx.coherenceDelta !== null ? `Coherence score moved ${ctx.coherenceDelta > 0 ? "+" : ""}${ctx.coherenceDelta}.` : ""),
|
|
3834
|
+
affectedBusinessAreas: ctx.affectedCapabilities.map((c) => c.name),
|
|
3835
|
+
riskLevel,
|
|
3836
|
+
riskJustification: hasCritical ? "Critical business capabilities were modified, requiring formal CAB sign-off." : `${ctx.affectedCapabilities.length} capabilities affected with potential for regression.`,
|
|
3837
|
+
complianceImpact: ctx.regulatoryCapabilities.length > 0 ? ctx.regulatoryCapabilities.join(", ") : "None",
|
|
3838
|
+
rollbackComplexity,
|
|
3839
|
+
rollbackGuidance: rollbackComplexity === "complex" ? "Prepare a full environment snapshot before deploying. Coordinate rollback with database team if schema changes exist." : rollbackComplexity === "moderate" ? "Tag the current deployment. Roll back using git revert on the merge commit." : "Revert the feature flag or redeploy the previous container image.",
|
|
3840
|
+
recommendedActions: [
|
|
3841
|
+
approvalRequired ? "Obtain formal CAB sign-off before deploying to production" : "Deploy during low-traffic window",
|
|
3842
|
+
ctx.breakingChanges.length > 0 ? "Notify API consumers of breaking changes at least 48h in advance" : "Run full regression suite",
|
|
3843
|
+
"Monitor error rates and coherence score for 30 minutes post-deploy"
|
|
3844
|
+
],
|
|
3845
|
+
approvalRequired,
|
|
3846
|
+
metrics: {
|
|
3847
|
+
commits: ctx.commitCount,
|
|
3848
|
+
filesChanged: ctx.filesChanged.length,
|
|
3849
|
+
capabilitiesAffected: ctx.affectedCapabilities.length,
|
|
3850
|
+
coherenceDelta: ctx.coherenceDelta
|
|
3851
|
+
}
|
|
3852
|
+
};
|
|
3853
|
+
}
|
|
3854
|
+
var RISK_LEVELS = ["low", "medium", "high", "critical"];
|
|
3855
|
+
var ROLLBACK_LEVELS = ["simple", "moderate", "complex"];
|
|
3856
|
+
function parseLlmResponse9(raw) {
|
|
3857
|
+
let text = raw.trim();
|
|
3858
|
+
if (text.startsWith("```")) {
|
|
3859
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
690
3860
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
3861
|
+
let parsed;
|
|
3862
|
+
try {
|
|
3863
|
+
parsed = JSON.parse(text);
|
|
3864
|
+
} catch {
|
|
3865
|
+
return null;
|
|
3866
|
+
}
|
|
3867
|
+
const riskLevel = RISK_LEVELS.includes(parsed.riskLevel) ? parsed.riskLevel : "medium";
|
|
3868
|
+
const rollbackComplexity = ROLLBACK_LEVELS.includes(
|
|
3869
|
+
parsed.rollbackComplexity
|
|
3870
|
+
) ? parsed.rollbackComplexity : "moderate";
|
|
3871
|
+
const recommendedActions = Array.isArray(parsed.recommendedActions) ? parsed.recommendedActions.filter((a) => typeof a === "string") : [];
|
|
3872
|
+
const affectedBusinessAreas = Array.isArray(parsed.affectedBusinessAreas) ? parsed.affectedBusinessAreas.filter((a) => typeof a === "string") : [];
|
|
3873
|
+
const metricsRaw = parsed.metrics && typeof parsed.metrics === "object" ? parsed.metrics : {};
|
|
696
3874
|
return {
|
|
697
|
-
|
|
698
|
-
|
|
3875
|
+
title: typeof parsed.title === "string" ? parsed.title : "Change Impact Brief",
|
|
3876
|
+
releaseVersion: typeof parsed.releaseVersion === "string" ? parsed.releaseVersion : "",
|
|
3877
|
+
date: typeof parsed.date === "string" ? parsed.date : (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
3878
|
+
audience: typeof parsed.audience === "string" ? parsed.audience : "",
|
|
3879
|
+
executiveSummary: typeof parsed.executiveSummary === "string" ? parsed.executiveSummary : "",
|
|
3880
|
+
technicalSummary: typeof parsed.technicalSummary === "string" ? parsed.technicalSummary : "",
|
|
3881
|
+
affectedBusinessAreas,
|
|
3882
|
+
riskLevel,
|
|
3883
|
+
riskJustification: typeof parsed.riskJustification === "string" ? parsed.riskJustification : "",
|
|
3884
|
+
complianceImpact: typeof parsed.complianceImpact === "string" ? parsed.complianceImpact : "None",
|
|
3885
|
+
rollbackComplexity,
|
|
3886
|
+
rollbackGuidance: typeof parsed.rollbackGuidance === "string" ? parsed.rollbackGuidance : "",
|
|
3887
|
+
recommendedActions,
|
|
3888
|
+
approvalRequired: parsed.approvalRequired === true,
|
|
3889
|
+
metrics: {
|
|
3890
|
+
commits: typeof metricsRaw.commits === "number" ? metricsRaw.commits : 0,
|
|
3891
|
+
filesChanged: typeof metricsRaw.filesChanged === "number" ? metricsRaw.filesChanged : 0,
|
|
3892
|
+
capabilitiesAffected: typeof metricsRaw.capabilitiesAffected === "number" ? metricsRaw.capabilitiesAffected : 0,
|
|
3893
|
+
coherenceDelta: typeof metricsRaw.coherenceDelta === "number" ? metricsRaw.coherenceDelta : null
|
|
3894
|
+
}
|
|
699
3895
|
};
|
|
700
3896
|
}
|
|
3897
|
+
async function generateChangeImpactBrief(opts) {
|
|
3898
|
+
const context = buildPromptContext2(opts);
|
|
3899
|
+
try {
|
|
3900
|
+
const response = await opts.llm.complete({
|
|
3901
|
+
systemPrompt: buildSystemPrompt15(opts.audience),
|
|
3902
|
+
messages: [{ role: "user", content: buildUserPrompt15(opts, context) }],
|
|
3903
|
+
maxTokens: 2048
|
|
3904
|
+
});
|
|
3905
|
+
const parsed = parseLlmResponse9(response.content);
|
|
3906
|
+
if (parsed) return parsed;
|
|
3907
|
+
} catch {
|
|
3908
|
+
}
|
|
3909
|
+
return buildFallback9(opts);
|
|
3910
|
+
}
|
|
701
3911
|
|
|
702
|
-
// src/generators/
|
|
703
|
-
var
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
|
|
708
|
-
email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
|
|
709
|
-
slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
|
|
710
|
-
slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
|
|
3912
|
+
// src/generators/generateRoiSlide.ts
|
|
3913
|
+
var CURRENCY_SYMBOL = {
|
|
3914
|
+
EUR: "\u20AC",
|
|
3915
|
+
USD: "$",
|
|
3916
|
+
GBP: "\xA3"
|
|
711
3917
|
};
|
|
712
|
-
function
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
3918
|
+
function fmtK(n, sym) {
|
|
3919
|
+
if (n >= 1e6) return `${sym}${(n / 1e6).toFixed(1)}M`;
|
|
3920
|
+
if (n >= 1e3) return `${sym}${Math.round(n / 1e3)}k`;
|
|
3921
|
+
return `${sym}${Math.round(n)}`;
|
|
3922
|
+
}
|
|
3923
|
+
function buildFallback10(opts) {
|
|
3924
|
+
const { roiEstimate: r, organizationName, currency } = opts;
|
|
3925
|
+
const sym = CURRENCY_SYMBOL[currency] ?? "\u20AC";
|
|
3926
|
+
const org = organizationName ?? "Your Organization";
|
|
3927
|
+
const costYr = fmtK(r.maintenanceCostPerYear, sym);
|
|
3928
|
+
const saveYr = fmtK(r.savedCostPerYear, sym);
|
|
3929
|
+
const saveMo = fmtK(r.savedCostPerMonth, sym);
|
|
3930
|
+
const slides = [
|
|
3931
|
+
{
|
|
3932
|
+
type: "title",
|
|
3933
|
+
title: "Technical Debt Cost Analysis",
|
|
3934
|
+
content: `${org} \xB7 Architecture Health Report \xB7 ${(/* @__PURE__ */ new Date()).toLocaleDateString("en-GB", { month: "long", year: "numeric" })}`,
|
|
3935
|
+
highlight: r.currentGrade,
|
|
3936
|
+
highlightLabel: "Current Architecture Grade"
|
|
3937
|
+
},
|
|
3938
|
+
{
|
|
3939
|
+
type: "problem",
|
|
3940
|
+
title: "The Hidden Cost of Architecture Debt",
|
|
3941
|
+
content: `Your codebase currently has grade ${r.currentGrade} (${r.currentScore}/100). This translates directly to developer time lost to maintenance overhead every month.`,
|
|
3942
|
+
highlight: fmtK(r.maintenanceCostPerMonth, sym),
|
|
3943
|
+
highlightLabel: "Estimated monthly maintenance cost",
|
|
3944
|
+
bullets: [
|
|
3945
|
+
`Import cycles: ${r.breakdown.importCycles.count} cycles consuming ${r.breakdown.importCycles.hoursPerMonth}h/mo`,
|
|
3946
|
+
`Documentation drift: ${r.breakdown.documentationDrift.count} drifted capabilities \u2014 ${r.breakdown.documentationDrift.hoursPerMonth}h/mo in context-searching`,
|
|
3947
|
+
`High-churn files: ${r.breakdown.highChurnFiles.count} hot files adding ${r.breakdown.highChurnFiles.hoursPerMonth}h/mo in review overhead`,
|
|
3948
|
+
`Orphaned capabilities: ${r.breakdown.orphanedCode.count} untracked modules \u2014 ${r.breakdown.orphanedCode.hoursPerMonth}h/mo wasted`
|
|
3949
|
+
]
|
|
3950
|
+
},
|
|
3951
|
+
{
|
|
3952
|
+
type: "cost-breakdown",
|
|
3953
|
+
title: "Where Developer Time Goes",
|
|
3954
|
+
content: `Total: ${r.estimatedMaintenanceHoursPerMonth}h/month in maintenance overhead = ${costYr}/year at current state`,
|
|
3955
|
+
bullets: [
|
|
3956
|
+
`Import cycles \u2192 ${fmtK(r.breakdown.importCycles.cost, sym)}/mo (${r.breakdown.importCycles.hoursPerMonth}h debugging)`,
|
|
3957
|
+
`Doc drift \u2192 ${fmtK(r.breakdown.documentationDrift.cost, sym)}/mo (${r.breakdown.documentationDrift.hoursPerMonth}h searching context)`,
|
|
3958
|
+
`Churn overhead \u2192 ${fmtK(r.breakdown.highChurnFiles.cost, sym)}/mo (${r.breakdown.highChurnFiles.hoursPerMonth}h in review)`,
|
|
3959
|
+
`Orphaned code \u2192 ${fmtK(r.breakdown.orphanedCode.cost, sym)}/mo (${r.breakdown.orphanedCode.hoursPerMonth}h ownership gaps)`
|
|
3960
|
+
]
|
|
3961
|
+
},
|
|
3962
|
+
{
|
|
3963
|
+
type: "scenarios",
|
|
3964
|
+
title: "What Reaching Grade B Saves",
|
|
3965
|
+
content: `Moving from ${r.currentGrade} to ${r.targetGrade} cuts maintenance overhead by ~${Math.round(r.savedCostPerMonth / Math.max(r.maintenanceCostPerMonth, 1) * 100)}%.`,
|
|
3966
|
+
highlight: saveYr,
|
|
3967
|
+
highlightLabel: `Annual savings at grade ${r.targetGrade}`,
|
|
3968
|
+
bullets: [
|
|
3969
|
+
`Monthly savings: ${saveMo}/mo`,
|
|
3970
|
+
`ROI multiple: ${r.roiMultiple}\xD7 return on remediation investment`,
|
|
3971
|
+
`Break-even: ~${r.breakEvenMonths < 1 ? `${Math.round(r.breakEvenMonths * 30)} days` : `${r.breakEvenMonths} months`}`,
|
|
3972
|
+
`Remediation effort: ~${r.estimatedRemediationDays} engineering days`
|
|
3973
|
+
]
|
|
3974
|
+
},
|
|
3975
|
+
{
|
|
3976
|
+
type: "recommendation",
|
|
3977
|
+
title: "Recommended Remediation Path",
|
|
3978
|
+
content: r.recommendation,
|
|
3979
|
+
bullets: [
|
|
3980
|
+
`Step 1: Resolve ${r.breakdown.importCycles.count} import cycles (est. ${Math.round(r.estimatedRemediationDays * 0.4)} days)`,
|
|
3981
|
+
`Step 2: Fix ${r.breakdown.documentationDrift.count} drifted capability docs (est. ${Math.round(r.estimatedRemediationDays * 0.3)} days)`,
|
|
3982
|
+
`Step 3: Assign ownership for ${r.breakdown.orphanedCode.count} orphaned capabilities (est. ${Math.round(r.estimatedRemediationDays * 0.2)} days)`,
|
|
3983
|
+
`Step 4: Add coverage to top churn files (est. ${Math.round(r.estimatedRemediationDays * 0.1)} days)`
|
|
3984
|
+
]
|
|
3985
|
+
},
|
|
3986
|
+
{
|
|
3987
|
+
type: "cta",
|
|
3988
|
+
title: `Approve ${(/* @__PURE__ */ new Date()).toLocaleString("default", { month: "short" })} Refactoring Budget`,
|
|
3989
|
+
content: `Investment: ~${fmtK(r.estimatedRemediationDays * 8 * 150, sym)} (${r.estimatedRemediationDays} days of engineering effort)
|
|
3990
|
+
Return: ${saveYr}/year in recovered developer capacity`,
|
|
3991
|
+
highlight: `${r.roiMultiple}\xD7`,
|
|
3992
|
+
highlightLabel: "ROI multiple",
|
|
3993
|
+
bullets: [
|
|
3994
|
+
`${saveYr}/year recurring savings`,
|
|
3995
|
+
`Break-even in ${r.breakEvenMonths < 1 ? `${Math.round(r.breakEvenMonths * 30)} days` : `${r.breakEvenMonths} months`}`,
|
|
3996
|
+
`Measurable via PRISM architecture grade (${r.currentGrade} \u2192 ${r.targetGrade})`
|
|
3997
|
+
]
|
|
3998
|
+
}
|
|
3999
|
+
];
|
|
4000
|
+
const executiveSummary = [
|
|
4001
|
+
`\u2022 Current architecture grade ${r.currentGrade} (${r.currentScore}/100) costs ~${costYr}/year in maintenance overhead.`,
|
|
4002
|
+
`\u2022 Improving to grade ${r.targetGrade} saves ~${saveYr}/year \u2014 ${r.roiMultiple}\xD7 return on a ${r.estimatedRemediationDays}-day remediation effort.`,
|
|
4003
|
+
`\u2022 Break-even: ~${r.breakEvenMonths < 1 ? `${Math.round(r.breakEvenMonths * 30)} days` : `${r.breakEvenMonths} months`}. Primary sources: ${r.breakdown.importCycles.count} import cycles, ${r.breakdown.documentationDrift.count} drifted docs, ${r.breakdown.orphanedCode.count} orphaned capabilities.`
|
|
4004
|
+
].join("\n");
|
|
4005
|
+
const oneLinePitch = `Moving from ${r.currentGrade} to ${r.targetGrade} saves ${saveYr} annually \u2014 ${r.roiMultiple}\xD7 ROI on a ${r.estimatedRemediationDays}-day investment.`;
|
|
4006
|
+
return {
|
|
4007
|
+
title: `Technical Debt ROI \u2014 ${org}`,
|
|
4008
|
+
slides,
|
|
4009
|
+
executiveSummary,
|
|
4010
|
+
oneLinePitch
|
|
718
4011
|
};
|
|
719
|
-
const guide = FORMAT_GUIDES[format];
|
|
720
|
-
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.`;
|
|
721
4012
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}[length];
|
|
732
|
-
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
733
|
-
const lines = [
|
|
734
|
-
`## User's Question`,
|
|
735
|
-
question,
|
|
4013
|
+
function buildSystemPrompt16() {
|
|
4014
|
+
return "You are a VP of Engineering creating a concise, business-value-focused presentation for C-suite audiences. Generate ONLY valid JSON \u2014 no markdown fences, no prose outside the JSON object.";
|
|
4015
|
+
}
|
|
4016
|
+
function buildUserPrompt16(opts) {
|
|
4017
|
+
const { roiEstimate: r, organizationName, currency } = opts;
|
|
4018
|
+
const sym = CURRENCY_SYMBOL[currency] ?? "\u20AC";
|
|
4019
|
+
const org = organizationName ?? "the organization";
|
|
4020
|
+
return [
|
|
4021
|
+
`Generate a 6-slide executive ROI presentation for ${org}.`,
|
|
736
4022
|
``,
|
|
737
|
-
`##
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
4023
|
+
`## ROI Data`,
|
|
4024
|
+
`Current grade: ${r.currentGrade} (score ${r.currentScore}/100)`,
|
|
4025
|
+
`Target grade: ${r.targetGrade} (score ${r.targetScore}/100)`,
|
|
4026
|
+
`Monthly maintenance cost: ${sym}${r.maintenanceCostPerMonth.toLocaleString()}`,
|
|
4027
|
+
`Annual maintenance cost: ${sym}${r.maintenanceCostPerYear.toLocaleString()}`,
|
|
4028
|
+
`Monthly savings if improved: ${sym}${r.savedCostPerMonth.toLocaleString()}`,
|
|
4029
|
+
`Annual savings if improved: ${sym}${r.savedCostPerYear.toLocaleString()}`,
|
|
4030
|
+
`ROI multiple: ${r.roiMultiple}x`,
|
|
4031
|
+
`Break-even: ${r.breakEvenMonths} months`,
|
|
4032
|
+
`Remediation days: ${r.estimatedRemediationDays}`,
|
|
741
4033
|
``,
|
|
742
|
-
`##
|
|
743
|
-
`
|
|
744
|
-
`
|
|
745
|
-
`
|
|
4034
|
+
`## Pain sources`,
|
|
4035
|
+
`Import cycles: ${r.breakdown.importCycles.count} cycles, ${r.breakdown.importCycles.hoursPerMonth}h/mo, ${sym}${r.breakdown.importCycles.cost.toLocaleString()}/mo`,
|
|
4036
|
+
`Doc drift: ${r.breakdown.documentationDrift.count} drifted, ${r.breakdown.documentationDrift.hoursPerMonth}h/mo, ${sym}${r.breakdown.documentationDrift.cost.toLocaleString()}/mo`,
|
|
4037
|
+
`High churn: ${r.breakdown.highChurnFiles.count} files, ${r.breakdown.highChurnFiles.hoursPerMonth}h/mo, ${sym}${r.breakdown.highChurnFiles.cost.toLocaleString()}/mo`,
|
|
4038
|
+
`Orphaned: ${r.breakdown.orphanedCode.count} capabilities, ${r.breakdown.orphanedCode.hoursPerMonth}h/mo, ${sym}${r.breakdown.orphanedCode.cost.toLocaleString()}/mo`,
|
|
746
4039
|
``,
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
4040
|
+
`## Output Format`,
|
|
4041
|
+
`Return a single JSON object with this exact shape:`,
|
|
4042
|
+
`{`,
|
|
4043
|
+
` "title": "<deck title>",`,
|
|
4044
|
+
` "slides": [`,
|
|
4045
|
+
` {`,
|
|
4046
|
+
` "type": "<title|problem|cost-breakdown|scenarios|recommendation|cta>",`,
|
|
4047
|
+
` "title": "<slide title>",`,
|
|
4048
|
+
` "content": "<1-2 sentences of slide body copy>",`,
|
|
4049
|
+
` "highlight": "<optional large callout \u2014 a number or grade>",`,
|
|
4050
|
+
` "highlightLabel": "<label for the highlight>",`,
|
|
4051
|
+
` "bullets": ["<bullet 1>", "<bullet 2>", "<bullet 3>"]`,
|
|
4052
|
+
` }`,
|
|
4053
|
+
` ],`,
|
|
4054
|
+
` "executiveSummary": "<3-bullet executive summary as a single string, bullets separated by \\n>",`,
|
|
4055
|
+
` "oneLinePitch": "<one sentence pitch for email subject line>"`,
|
|
4056
|
+
`}`,
|
|
4057
|
+
``,
|
|
4058
|
+
`Rules:`,
|
|
4059
|
+
`- Exactly 6 slides in order: title, problem, cost-breakdown, scenarios, recommendation, cta`,
|
|
4060
|
+
`- bullets: max 4 per slide, each \u2264 90 characters`,
|
|
4061
|
+
`- content: 1\u20132 sentences, specific numbers from the data above`,
|
|
4062
|
+
`- Use business language \u2014 avoid engineering jargon`,
|
|
4063
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
4064
|
+
].join("\n");
|
|
756
4065
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
762
|
-
};
|
|
4066
|
+
function parseResponse(raw) {
|
|
4067
|
+
let text = raw.trim();
|
|
4068
|
+
if (text.startsWith("```")) {
|
|
4069
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
763
4070
|
}
|
|
764
|
-
|
|
4071
|
+
try {
|
|
4072
|
+
const parsed = JSON.parse(text);
|
|
4073
|
+
const rawSlides = Array.isArray(parsed.slides) ? parsed.slides : [];
|
|
4074
|
+
if (rawSlides.length === 0) return null;
|
|
4075
|
+
const VALID_TYPES = ["title", "problem", "cost-breakdown", "scenarios", "recommendation", "cta"];
|
|
4076
|
+
const slides = rawSlides.map((s, i) => {
|
|
4077
|
+
const slide = s ?? {};
|
|
4078
|
+
const type = typeof slide.type === "string" && VALID_TYPES.includes(slide.type) ? slide.type : VALID_TYPES[i] ?? "recommendation";
|
|
4079
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets.filter((b) => typeof b === "string").slice(0, 4) : void 0;
|
|
4080
|
+
return {
|
|
4081
|
+
type,
|
|
4082
|
+
title: typeof slide.title === "string" ? slide.title : `Slide ${i + 1}`,
|
|
4083
|
+
content: typeof slide.content === "string" ? slide.content : "",
|
|
4084
|
+
...typeof slide.highlight === "string" && slide.highlight ? { highlight: slide.highlight } : {},
|
|
4085
|
+
...typeof slide.highlightLabel === "string" && slide.highlightLabel ? { highlightLabel: slide.highlightLabel } : {},
|
|
4086
|
+
...bullets && bullets.length > 0 ? { bullets } : {}
|
|
4087
|
+
};
|
|
4088
|
+
});
|
|
765
4089
|
return {
|
|
766
|
-
|
|
767
|
-
|
|
4090
|
+
title: typeof parsed.title === "string" ? parsed.title : "Technical Debt ROI",
|
|
4091
|
+
slides,
|
|
4092
|
+
executiveSummary: typeof parsed.executiveSummary === "string" ? parsed.executiveSummary : "",
|
|
4093
|
+
oneLinePitch: typeof parsed.oneLinePitch === "string" ? parsed.oneLinePitch : ""
|
|
768
4094
|
};
|
|
4095
|
+
} catch {
|
|
4096
|
+
return null;
|
|
769
4097
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
}
|
|
4098
|
+
}
|
|
4099
|
+
async function generateRoiSlide(opts) {
|
|
4100
|
+
try {
|
|
4101
|
+
const response = await opts.llm.complete({
|
|
4102
|
+
systemPrompt: buildSystemPrompt16(),
|
|
4103
|
+
messages: [{ role: "user", content: buildUserPrompt16(opts) }],
|
|
4104
|
+
maxTokens: 3e3
|
|
4105
|
+
});
|
|
4106
|
+
const parsed = parseResponse(response.content);
|
|
4107
|
+
if (parsed) return parsed;
|
|
4108
|
+
} catch {
|
|
4109
|
+
}
|
|
4110
|
+
return buildFallback10(opts);
|
|
782
4111
|
}
|
|
783
4112
|
|
|
784
4113
|
// src/forge/types.ts
|
|
@@ -1705,63 +5034,63 @@ function initialFormValues(fields, source) {
|
|
|
1705
5034
|
function isEmptyString(v) {
|
|
1706
5035
|
return typeof v === "string" && v.trim().length === 0;
|
|
1707
5036
|
}
|
|
1708
|
-
function validateField(field, value,
|
|
5037
|
+
function validateField(field, value, path5) {
|
|
1709
5038
|
const errors = {};
|
|
1710
5039
|
switch (field.kind) {
|
|
1711
5040
|
case "string": {
|
|
1712
5041
|
if (field.required && (value === void 0 || value === null || isEmptyString(value))) {
|
|
1713
|
-
errors[
|
|
5042
|
+
errors[path5] = `${field.label} ist erforderlich`;
|
|
1714
5043
|
break;
|
|
1715
5044
|
}
|
|
1716
5045
|
if (typeof value === "string") {
|
|
1717
5046
|
if (field.minLength !== void 0 && value.length < field.minLength) {
|
|
1718
|
-
errors[
|
|
5047
|
+
errors[path5] = `${field.label} mindestens ${field.minLength} Zeichen`;
|
|
1719
5048
|
} else if (field.maxLength !== void 0 && value.length > field.maxLength) {
|
|
1720
|
-
errors[
|
|
5049
|
+
errors[path5] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
|
|
1721
5050
|
} else if (field.enum && value.length > 0 && !field.enum.includes(value)) {
|
|
1722
|
-
errors[
|
|
5051
|
+
errors[path5] = `${field.label}: Wert nicht in der Auswahl`;
|
|
1723
5052
|
}
|
|
1724
5053
|
}
|
|
1725
5054
|
break;
|
|
1726
5055
|
}
|
|
1727
5056
|
case "number": {
|
|
1728
5057
|
if (value === "" || value === void 0 || value === null) {
|
|
1729
|
-
if (field.required) errors[
|
|
5058
|
+
if (field.required) errors[path5] = `${field.label} ist erforderlich`;
|
|
1730
5059
|
break;
|
|
1731
5060
|
}
|
|
1732
5061
|
const n = typeof value === "number" ? value : Number(value);
|
|
1733
5062
|
if (!Number.isFinite(n)) {
|
|
1734
|
-
errors[
|
|
5063
|
+
errors[path5] = `${field.label} muss eine Zahl sein`;
|
|
1735
5064
|
} else if (field.integer && !Number.isInteger(n)) {
|
|
1736
|
-
errors[
|
|
5065
|
+
errors[path5] = `${field.label} muss eine ganze Zahl sein`;
|
|
1737
5066
|
} else if (field.minimum !== void 0 && n < field.minimum) {
|
|
1738
|
-
errors[
|
|
5067
|
+
errors[path5] = `${field.label} >= ${field.minimum}`;
|
|
1739
5068
|
} else if (field.maximum !== void 0 && n > field.maximum) {
|
|
1740
|
-
errors[
|
|
5069
|
+
errors[path5] = `${field.label} <= ${field.maximum}`;
|
|
1741
5070
|
}
|
|
1742
5071
|
break;
|
|
1743
5072
|
}
|
|
1744
5073
|
case "boolean":
|
|
1745
5074
|
if (field.required && value !== true) {
|
|
1746
|
-
errors[
|
|
5075
|
+
errors[path5] = `${field.label} muss aktiviert sein`;
|
|
1747
5076
|
}
|
|
1748
5077
|
break;
|
|
1749
5078
|
case "array": {
|
|
1750
5079
|
const arr = Array.isArray(value) ? value : [];
|
|
1751
5080
|
if (field.required && arr.length === 0) {
|
|
1752
|
-
errors[
|
|
5081
|
+
errors[path5] = `${field.label}: mindestens ein Eintrag erforderlich`;
|
|
1753
5082
|
}
|
|
1754
5083
|
if (field.minItems !== void 0 && arr.length < field.minItems) {
|
|
1755
|
-
errors[
|
|
5084
|
+
errors[path5] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
|
|
1756
5085
|
} else if (field.maxItems !== void 0 && arr.length > field.maxItems) {
|
|
1757
|
-
errors[
|
|
5086
|
+
errors[path5] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
|
|
1758
5087
|
}
|
|
1759
5088
|
if (field.itemField) {
|
|
1760
5089
|
arr.forEach((item, i) => {
|
|
1761
5090
|
const sub = validateField(
|
|
1762
5091
|
field.itemField,
|
|
1763
5092
|
item,
|
|
1764
|
-
`${
|
|
5093
|
+
`${path5}[${i}]`
|
|
1765
5094
|
);
|
|
1766
5095
|
Object.assign(errors, sub);
|
|
1767
5096
|
});
|
|
@@ -1771,7 +5100,7 @@ function validateField(field, value, path3) {
|
|
|
1771
5100
|
case "object": {
|
|
1772
5101
|
const obj = isPlainObject5(value) ? value : {};
|
|
1773
5102
|
for (const sub of field.fields) {
|
|
1774
|
-
const subErrors = validateField(sub, obj[sub.name], `${
|
|
5103
|
+
const subErrors = validateField(sub, obj[sub.name], `${path5}.${sub.name}`);
|
|
1775
5104
|
Object.assign(errors, subErrors);
|
|
1776
5105
|
}
|
|
1777
5106
|
break;
|
|
@@ -2266,6 +5595,80 @@ var WIDGET_TEMPLATE_SOCIAL_PROOF = {
|
|
|
2266
5595
|
{ id: "show_divider", label: "Show divider", type: "select", options: ["yes", "no"], default: "no" }
|
|
2267
5596
|
]
|
|
2268
5597
|
};
|
|
5598
|
+
var WIDGET_TEMPLATE_ANIMATED_STAT = {
|
|
5599
|
+
id: "animated-stat",
|
|
5600
|
+
name: "Animated Stat",
|
|
5601
|
+
description: "Full-card animated number counter. Perfect for LinkedIn/Instagram. Export as video or GIF.",
|
|
5602
|
+
free_tier: false,
|
|
5603
|
+
animated: true,
|
|
5604
|
+
animDurationMs: 2200,
|
|
5605
|
+
videoDurationMs: 3800,
|
|
5606
|
+
defaultWidth: 600,
|
|
5607
|
+
defaultHeight: 400,
|
|
5608
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
5609
|
+
slots: [
|
|
5610
|
+
{ id: "value", label: "Value (number)", type: "number", required: true, placeholder: "138", default: "138" },
|
|
5611
|
+
{ id: "label", label: "Label", type: "text", required: true, placeholder: "Files analysed", default: "Files analysed" },
|
|
5612
|
+
{ id: "unit", label: "Unit (optional)", type: "text", placeholder: "k LOC", default: "" },
|
|
5613
|
+
{ id: "sublabel", label: "Sublabel / context", type: "text", placeholder: "across 3 modules", default: "" },
|
|
5614
|
+
{ id: "delta", label: "Delta text", type: "text", placeholder: "+22% this sprint", default: "" },
|
|
5615
|
+
{ id: "delta_direction", label: "Delta direction", type: "select", options: ["up", "down", "neutral"], default: "up" }
|
|
5616
|
+
]
|
|
5617
|
+
};
|
|
5618
|
+
var WIDGET_TEMPLATE_RELEASE_CARD = {
|
|
5619
|
+
id: "release-card",
|
|
5620
|
+
name: "Release Card",
|
|
5621
|
+
description: "Animated release announcement \u2014 version, headline, and staggered change list. Great for LinkedIn posts.",
|
|
5622
|
+
free_tier: false,
|
|
5623
|
+
animated: true,
|
|
5624
|
+
animDurationMs: 1200,
|
|
5625
|
+
videoDurationMs: 3e3,
|
|
5626
|
+
defaultWidth: 600,
|
|
5627
|
+
defaultHeight: 400,
|
|
5628
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
5629
|
+
slots: [
|
|
5630
|
+
{ id: "version", label: "Version", type: "text", required: true, placeholder: "v2.4.0", default: "v2.4.0" },
|
|
5631
|
+
{ id: "headline", label: "Headline", type: "text", required: true, placeholder: "Faster. Smarter. Leaner.", default: "Faster. Smarter. Leaner." },
|
|
5632
|
+
{ 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" },
|
|
5633
|
+
{ id: "tag", label: "Release tag", type: "select", options: ["feat", "fix", "perf", "break", ""], default: "feat" }
|
|
5634
|
+
]
|
|
5635
|
+
};
|
|
5636
|
+
var WIDGET_TEMPLATE_CHART_BARS = {
|
|
5637
|
+
id: "chart-bars",
|
|
5638
|
+
name: "Chart \u2014 Bars",
|
|
5639
|
+
description: "Animated Chart.js bar chart from your codebase metrics. Perfect for visualising churn, coverage, or LOC.",
|
|
5640
|
+
free_tier: false,
|
|
5641
|
+
animated: true,
|
|
5642
|
+
animDurationMs: 1600,
|
|
5643
|
+
videoDurationMs: 3200,
|
|
5644
|
+
defaultWidth: 640,
|
|
5645
|
+
defaultHeight: 420,
|
|
5646
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
5647
|
+
slots: [
|
|
5648
|
+
{ id: "title", label: "Chart title", type: "text", placeholder: "Files by module", default: "Files by module" },
|
|
5649
|
+
{ id: "labels", label: "Labels (one per line)", type: "multiline", required: true, placeholder: "api\nlib\ncomponents\napp", default: "api\nlib\ncomponents\napp" },
|
|
5650
|
+
{ id: "values", label: "Values (one per line)", type: "multiline", required: true, placeholder: "24\n18\n41\n12", default: "24\n18\n41\n12" },
|
|
5651
|
+
{ id: "orientation", label: "Orientation", type: "select", options: ["vertical", "horizontal"], default: "vertical" },
|
|
5652
|
+
{ id: "accent_color", label: "Bar colour (CSS)", type: "color", placeholder: "#f97316", default: "" }
|
|
5653
|
+
]
|
|
5654
|
+
};
|
|
5655
|
+
var WIDGET_TEMPLATE_ARCHITECTURE_BADGE = {
|
|
5656
|
+
id: "architecture-badge",
|
|
5657
|
+
name: "Architecture Badge",
|
|
5658
|
+
description: "Animated tech-stack showcase with Lucide icons. Items pop in with spring animation \u2014 great for profile posts.",
|
|
5659
|
+
free_tier: false,
|
|
5660
|
+
animated: true,
|
|
5661
|
+
animDurationMs: 900,
|
|
5662
|
+
videoDurationMs: 2500,
|
|
5663
|
+
defaultWidth: 580,
|
|
5664
|
+
defaultHeight: 360,
|
|
5665
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
5666
|
+
slots: [
|
|
5667
|
+
{ id: "title", label: "Title", type: "text", placeholder: "Built with", default: "Built with" },
|
|
5668
|
+
{ id: "items", label: "Stack items (one per line)", type: "multiline", required: true, placeholder: "TypeScript\nNext.js\nPostgres\nTailwind", default: "TypeScript\nNext.js\nPostgres\nTailwind" },
|
|
5669
|
+
{ id: "subtitle", label: "Subtitle / tagline", type: "text", placeholder: "100% local-first \xB7 zero telemetry", default: "" }
|
|
5670
|
+
]
|
|
5671
|
+
};
|
|
2269
5672
|
var BUNDLED_WIDGET_TEMPLATES = [
|
|
2270
5673
|
WIDGET_TEMPLATE_STAT_CARD,
|
|
2271
5674
|
WIDGET_TEMPLATE_FEATURE_GRID,
|
|
@@ -2274,7 +5677,12 @@ var BUNDLED_WIDGET_TEMPLATES = [
|
|
|
2274
5677
|
WIDGET_TEMPLATE_METRIC_BADGE,
|
|
2275
5678
|
WIDGET_TEMPLATE_PRICING_TIER,
|
|
2276
5679
|
WIDGET_TEMPLATE_CHANGELOG_ROW,
|
|
2277
|
-
WIDGET_TEMPLATE_SOCIAL_PROOF
|
|
5680
|
+
WIDGET_TEMPLATE_SOCIAL_PROOF,
|
|
5681
|
+
// Animated — standalone + video export
|
|
5682
|
+
WIDGET_TEMPLATE_ANIMATED_STAT,
|
|
5683
|
+
WIDGET_TEMPLATE_RELEASE_CARD,
|
|
5684
|
+
WIDGET_TEMPLATE_CHART_BARS,
|
|
5685
|
+
WIDGET_TEMPLATE_ARCHITECTURE_BADGE
|
|
2278
5686
|
];
|
|
2279
5687
|
var FREE_TIER_WIDGET_IDS = BUNDLED_WIDGET_TEMPLATES.filter((t) => t.free_tier).map((t) => t.id);
|
|
2280
5688
|
function getWidgetTemplate(id) {
|
|
@@ -2433,6 +5841,184 @@ ${divider ? `<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);m
|
|
|
2433
5841
|
${logos.map((l) => ` <span style="font-weight:600;font-size:.875rem;color:var(--pw-brand-muted);letter-spacing:-.01em">${escHtml(l)}</span>`).join("\n")}
|
|
2434
5842
|
</div>`;
|
|
2435
5843
|
}
|
|
5844
|
+
function renderAnimatedStatHtml(slots, t) {
|
|
5845
|
+
const value = getSlotValue(slots, t, "value");
|
|
5846
|
+
const label = getSlotValue(slots, t, "label");
|
|
5847
|
+
const unit = getSlotValue(slots, t, "unit");
|
|
5848
|
+
const sublabel = getSlotValue(slots, t, "sublabel");
|
|
5849
|
+
const dir = getSlotValue(slots, t, "delta_direction");
|
|
5850
|
+
const delta = getSlotValue(slots, t, "delta");
|
|
5851
|
+
const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
|
|
5852
|
+
const deltaColor = dir === "up" ? "#22c55e" : dir === "down" ? "#ef4444" : "var(--pw-brand-muted)";
|
|
5853
|
+
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)">
|
|
5854
|
+
<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>
|
|
5855
|
+
${unit ? `<div style="font-size:1.25rem;font-weight:600;color:var(--pw-brand-muted);margin-top:-var(--pw-spacing-sm)">${escHtml(unit)}</div>` : ""}
|
|
5856
|
+
<div style="font-size:1.1rem;font-weight:600;color:var(--pw-brand-text);letter-spacing:0.01em">${escHtml(label)}</div>
|
|
5857
|
+
${sublabel ? `<div style="font-size:.8rem;color:var(--pw-brand-muted)">${escHtml(sublabel)}</div>` : ""}
|
|
5858
|
+
${delta ? `<div style="font-size:.875rem;color:${deltaColor};font-weight:600">${arrow}${arrow ? " " : ""}${escHtml(delta)}</div>` : ""}
|
|
5859
|
+
</div>
|
|
5860
|
+
<script>
|
|
5861
|
+
(function(){
|
|
5862
|
+
var target = ${Number(value.replace(/[^0-9.-]/g, "")) || 0};
|
|
5863
|
+
var isFloat = target % 1 !== 0;
|
|
5864
|
+
var obj = { val: 0 };
|
|
5865
|
+
anime({
|
|
5866
|
+
targets: obj,
|
|
5867
|
+
val: target,
|
|
5868
|
+
duration: ${2e3},
|
|
5869
|
+
easing: "easeOutExpo",
|
|
5870
|
+
update: function() {
|
|
5871
|
+
document.getElementById("pw-anim-value").textContent =
|
|
5872
|
+
isFloat ? obj.val.toFixed(1) : Math.round(obj.val).toLocaleString();
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5875
|
+
})();
|
|
5876
|
+
</script>`;
|
|
5877
|
+
}
|
|
5878
|
+
function renderReleaseCardHtml(slots, t) {
|
|
5879
|
+
const version = getSlotValue(slots, t, "version");
|
|
5880
|
+
const headline = getSlotValue(slots, t, "headline");
|
|
5881
|
+
const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
|
|
5882
|
+
const tag = getSlotValue(slots, t, "tag");
|
|
5883
|
+
const tagColors = { feat: "#22c55e", fix: "#f59e0b", perf: "#60a5fa", break: "#ef4444" };
|
|
5884
|
+
const tc = tagColors[tag] ?? "var(--pw-brand-accent)";
|
|
5885
|
+
return `<div class="pw-release-wrap" style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
|
|
5886
|
+
<div style="display:flex;align-items:center;gap:var(--pw-spacing-sm);opacity:0" class="pw-ri">
|
|
5887
|
+
<span style="font-family:var(--pw-font-mono);font-size:1.1rem;font-weight:700;color:var(--pw-brand-primary)">${escHtml(version)}</span>
|
|
5888
|
+
${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>` : ""}
|
|
5889
|
+
</div>
|
|
5890
|
+
<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>
|
|
5891
|
+
<ul style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--pw-spacing-xs)">
|
|
5892
|
+
${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 ")}
|
|
5893
|
+
</ul>
|
|
5894
|
+
</div>
|
|
5895
|
+
<script>
|
|
5896
|
+
(function(){
|
|
5897
|
+
anime({ targets: ".pw-ri", translateY: [-16, 0], opacity: [0, 1], delay: anime.stagger(120), duration: 500, easing: "easeOutCubic" });
|
|
5898
|
+
anime({ targets: ".pw-change-item", translateX: [-20, 0], opacity: [0, 1], delay: anime.stagger(80, { start: 350 }), duration: 450, easing: "easeOutCubic" });
|
|
5899
|
+
})();
|
|
5900
|
+
</script>`;
|
|
5901
|
+
}
|
|
5902
|
+
function renderChartBarsHtml(slots, t) {
|
|
5903
|
+
const title = getSlotValue(slots, t, "title");
|
|
5904
|
+
const labelsRaw = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
|
|
5905
|
+
const valuesRaw = getSlotValue(slots, t, "values").split("\n").filter(Boolean).map(Number);
|
|
5906
|
+
const orientation = getSlotValue(slots, t, "orientation") || "vertical";
|
|
5907
|
+
getSlotValue(slots, t, "accent_color") || "var(--pw-brand-primary)";
|
|
5908
|
+
const labelsJson = JSON.stringify(labelsRaw);
|
|
5909
|
+
const valuesJson = JSON.stringify(valuesRaw);
|
|
5910
|
+
const chartType = orientation === "horizontal" ? "bar" : "bar";
|
|
5911
|
+
const indexAxis = orientation === "horizontal" ? `indexAxis: "y",` : "";
|
|
5912
|
+
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>` : ""}
|
|
5913
|
+
<div style="position:relative;width:100%;height:220px">
|
|
5914
|
+
<canvas id="pw-chart" style="width:100%;height:100%"></canvas>
|
|
5915
|
+
</div>
|
|
5916
|
+
<script>
|
|
5917
|
+
(function(){
|
|
5918
|
+
var ctx = document.getElementById("pw-chart").getContext("2d");
|
|
5919
|
+
var accent = getComputedStyle(document.querySelector(".pw-widget")).getPropertyValue("--pw-brand-primary").trim() || "#f97316";
|
|
5920
|
+
new Chart(ctx, {
|
|
5921
|
+
type: "${chartType}",
|
|
5922
|
+
data: {
|
|
5923
|
+
labels: ${labelsJson},
|
|
5924
|
+
datasets: [{
|
|
5925
|
+
data: ${valuesJson},
|
|
5926
|
+
backgroundColor: accent + "cc",
|
|
5927
|
+
borderColor: accent,
|
|
5928
|
+
borderWidth: 2,
|
|
5929
|
+
borderRadius: 6,
|
|
5930
|
+
}]
|
|
5931
|
+
},
|
|
5932
|
+
options: {
|
|
5933
|
+
${indexAxis}
|
|
5934
|
+
responsive: true,
|
|
5935
|
+
maintainAspectRatio: false,
|
|
5936
|
+
animation: { duration: 1500, easing: "easeOutQuart" },
|
|
5937
|
+
plugins: { legend: { display: false } },
|
|
5938
|
+
scales: {
|
|
5939
|
+
x: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } },
|
|
5940
|
+
y: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } }
|
|
5941
|
+
}
|
|
5942
|
+
}
|
|
5943
|
+
});
|
|
5944
|
+
})();
|
|
5945
|
+
</script>`;
|
|
5946
|
+
}
|
|
5947
|
+
var LUCIDE_ICON_MAP = {
|
|
5948
|
+
typescript: "code-2",
|
|
5949
|
+
javascript: "code-2",
|
|
5950
|
+
"js": "code-2",
|
|
5951
|
+
"ts": "code-2",
|
|
5952
|
+
react: "atom",
|
|
5953
|
+
next: "triangle",
|
|
5954
|
+
nextjs: "triangle",
|
|
5955
|
+
"next.js": "triangle",
|
|
5956
|
+
node: "server",
|
|
5957
|
+
nodejs: "server",
|
|
5958
|
+
"node.js": "server",
|
|
5959
|
+
postgres: "database",
|
|
5960
|
+
postgresql: "database",
|
|
5961
|
+
mysql: "database",
|
|
5962
|
+
sqlite: "database",
|
|
5963
|
+
supabase: "database",
|
|
5964
|
+
prisma: "layers",
|
|
5965
|
+
tailwind: "palette",
|
|
5966
|
+
css: "palette",
|
|
5967
|
+
sass: "palette",
|
|
5968
|
+
docker: "box",
|
|
5969
|
+
kubernetes: "cloud",
|
|
5970
|
+
aws: "cloud",
|
|
5971
|
+
vercel: "triangle",
|
|
5972
|
+
github: "git-branch",
|
|
5973
|
+
git: "git-branch",
|
|
5974
|
+
graphql: "share-2",
|
|
5975
|
+
rest: "globe",
|
|
5976
|
+
api: "globe",
|
|
5977
|
+
redis: "zap",
|
|
5978
|
+
kafka: "radio",
|
|
5979
|
+
python: "terminal",
|
|
5980
|
+
rust: "cpu",
|
|
5981
|
+
go: "activity",
|
|
5982
|
+
java: "coffee",
|
|
5983
|
+
vue: "layers",
|
|
5984
|
+
svelte: "layers",
|
|
5985
|
+
angular: "layers",
|
|
5986
|
+
figma: "pen-tool",
|
|
5987
|
+
linear: "layers",
|
|
5988
|
+
default: "layers"
|
|
5989
|
+
};
|
|
5990
|
+
function renderArchitectureBadgeHtml(slots, t) {
|
|
5991
|
+
const title = getSlotValue(slots, t, "title");
|
|
5992
|
+
const subtitle = getSlotValue(slots, t, "subtitle");
|
|
5993
|
+
const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
|
|
5994
|
+
const palette = ["#60a5fa", "#f97316", "#34d399", "#a78bfa", "#fb923c", "#38bdf8", "#facc15", "#f472b6"];
|
|
5995
|
+
const chips = items.map((item, i) => {
|
|
5996
|
+
const key = item.trim().toLowerCase().replace(/[^a-z0-9.]/g, "");
|
|
5997
|
+
const icon = LUCIDE_ICON_MAP[key] ?? LUCIDE_ICON_MAP.default;
|
|
5998
|
+
const color = palette[i % palette.length];
|
|
5999
|
+
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">
|
|
6000
|
+
<i data-lucide="${icon}" style="width:14px;height:14px;color:${color};flex-shrink:0"></i>
|
|
6001
|
+
<span style="font-size:.8rem;font-weight:600;color:${color};letter-spacing:.02em">${escHtml(item.trim())}</span>
|
|
6002
|
+
</div>`;
|
|
6003
|
+
}).join("\n ");
|
|
6004
|
+
return `<div style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
|
|
6005
|
+
${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>` : ""}
|
|
6006
|
+
<div style="display:flex;flex-wrap:wrap;gap:var(--pw-spacing-xs)">
|
|
6007
|
+
${chips}
|
|
6008
|
+
</div>
|
|
6009
|
+
${subtitle ? `<p style="margin:0;font-size:.75rem;color:var(--pw-brand-muted);opacity:0" class="pw-arch-sub">${escHtml(subtitle)}</p>` : ""}
|
|
6010
|
+
</div>
|
|
6011
|
+
<script>
|
|
6012
|
+
(function(){
|
|
6013
|
+
lucide.createIcons();
|
|
6014
|
+
var title = document.getElementById("pw-arch-title");
|
|
6015
|
+
if(title) anime({ targets: title, opacity: [0,1], translateY: [-10,0], duration: 400, easing: "easeOutCubic" });
|
|
6016
|
+
anime({ targets: ".pw-arch-item", opacity: [0,1], scale: [0.85,1], delay: anime.stagger(70, { start: 200 }), duration: 400, easing: "easeOutBack" });
|
|
6017
|
+
var sub = document.querySelector(".pw-arch-sub");
|
|
6018
|
+
if(sub) anime({ targets: sub, opacity: [0,1], delay: 600, duration: 400, easing: "easeOutCubic" });
|
|
6019
|
+
})();
|
|
6020
|
+
</script>`;
|
|
6021
|
+
}
|
|
2436
6022
|
var HTML_BODY_FN = {
|
|
2437
6023
|
"stat-card": renderStatCardHtml,
|
|
2438
6024
|
"feature-grid": renderFeatureGridHtml,
|
|
@@ -2441,7 +6027,11 @@ var HTML_BODY_FN = {
|
|
|
2441
6027
|
"metric-badge": renderMetricBadgeHtml,
|
|
2442
6028
|
"pricing-tier": renderPricingTierHtml,
|
|
2443
6029
|
"changelog-row": renderChangelogRowHtml,
|
|
2444
|
-
"social-proof": renderSocialProofHtml
|
|
6030
|
+
"social-proof": renderSocialProofHtml,
|
|
6031
|
+
"animated-stat": renderAnimatedStatHtml,
|
|
6032
|
+
"release-card": renderReleaseCardHtml,
|
|
6033
|
+
"chart-bars": renderChartBarsHtml,
|
|
6034
|
+
"architecture-badge": renderArchitectureBadgeHtml
|
|
2445
6035
|
};
|
|
2446
6036
|
function renderStatCardMd(slots, t) {
|
|
2447
6037
|
const value = getSlotValue(slots, t, "value");
|
|
@@ -2512,6 +6102,43 @@ function renderSocialProofMd(slots, t) {
|
|
|
2512
6102
|
const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
|
|
2513
6103
|
return `${caption ? caption + "\n\n" : ""}${logos.join(" \xB7 ")}`;
|
|
2514
6104
|
}
|
|
6105
|
+
function renderAnimatedStatMd(slots, t) {
|
|
6106
|
+
const value = getSlotValue(slots, t, "value");
|
|
6107
|
+
const label = getSlotValue(slots, t, "label");
|
|
6108
|
+
const unit = getSlotValue(slots, t, "unit");
|
|
6109
|
+
const delta = getSlotValue(slots, t, "delta");
|
|
6110
|
+
const dir = getSlotValue(slots, t, "delta_direction");
|
|
6111
|
+
const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
|
|
6112
|
+
return `**${value}${unit ? " " + unit : ""}** \u2014 ${label}${delta ? ` ${arrow} ${delta}` : ""}`;
|
|
6113
|
+
}
|
|
6114
|
+
function renderReleaseCardMd(slots, t) {
|
|
6115
|
+
const version = getSlotValue(slots, t, "version");
|
|
6116
|
+
const headline = getSlotValue(slots, t, "headline");
|
|
6117
|
+
const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
|
|
6118
|
+
const tag = getSlotValue(slots, t, "tag");
|
|
6119
|
+
return `### ${version}${tag ? ` \`${tag}\`` : ""} \u2014 ${headline}
|
|
6120
|
+
|
|
6121
|
+
${changes.map((c) => `- ${c}`).join("\n")}`;
|
|
6122
|
+
}
|
|
6123
|
+
function renderChartBarsMd(slots, t) {
|
|
6124
|
+
const title = getSlotValue(slots, t, "title");
|
|
6125
|
+
const labels = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
|
|
6126
|
+
const values = getSlotValue(slots, t, "values").split("\n").filter(Boolean);
|
|
6127
|
+
const rows = labels.map((l, i) => `- **${l}**: ${values[i] ?? "0"}`).join("\n");
|
|
6128
|
+
return `${title ? `## ${title}
|
|
6129
|
+
|
|
6130
|
+
` : ""}${rows}`;
|
|
6131
|
+
}
|
|
6132
|
+
function renderArchitectureBadgeMd(slots, t) {
|
|
6133
|
+
const title = getSlotValue(slots, t, "title");
|
|
6134
|
+
const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
|
|
6135
|
+
const subtitle = getSlotValue(slots, t, "subtitle");
|
|
6136
|
+
return `${title ? `**${title}**
|
|
6137
|
+
|
|
6138
|
+
` : ""}${items.map((i) => `- ${i.trim()}`).join("\n")}${subtitle ? `
|
|
6139
|
+
|
|
6140
|
+
*${subtitle}*` : ""}`;
|
|
6141
|
+
}
|
|
2515
6142
|
var MD_FN = {
|
|
2516
6143
|
"stat-card": renderStatCardMd,
|
|
2517
6144
|
"feature-grid": renderFeatureGridMd,
|
|
@@ -2520,7 +6147,11 @@ var MD_FN = {
|
|
|
2520
6147
|
"metric-badge": renderMetricBadgeMd,
|
|
2521
6148
|
"pricing-tier": renderPricingTierMd,
|
|
2522
6149
|
"changelog-row": renderChangelogRowMd,
|
|
2523
|
-
"social-proof": renderSocialProofMd
|
|
6150
|
+
"social-proof": renderSocialProofMd,
|
|
6151
|
+
"animated-stat": renderAnimatedStatMd,
|
|
6152
|
+
"release-card": renderReleaseCardMd,
|
|
6153
|
+
"chart-bars": renderChartBarsMd,
|
|
6154
|
+
"architecture-badge": renderArchitectureBadgeMd
|
|
2524
6155
|
};
|
|
2525
6156
|
function toReactStyle(vars) {
|
|
2526
6157
|
return "{\n" + Object.entries(vars).map(([k, v]) => ` "${k}": "${v.replace(/"/g, '\\"')}"`).join(",\n") + "\n }";
|
|
@@ -2552,6 +6183,89 @@ ${slotDecls}
|
|
|
2552
6183
|
}
|
|
2553
6184
|
`;
|
|
2554
6185
|
}
|
|
6186
|
+
function getRequiredLibs(templateId) {
|
|
6187
|
+
const libs = /* @__PURE__ */ new Set(["tailwind"]);
|
|
6188
|
+
if (["animated-stat", "release-card"].includes(templateId)) {
|
|
6189
|
+
libs.add("anime");
|
|
6190
|
+
}
|
|
6191
|
+
if (templateId === "chart-bars") {
|
|
6192
|
+
libs.add("anime");
|
|
6193
|
+
libs.add("chartjs");
|
|
6194
|
+
}
|
|
6195
|
+
if (templateId === "architecture-badge") {
|
|
6196
|
+
libs.add("anime");
|
|
6197
|
+
libs.add("lucide");
|
|
6198
|
+
}
|
|
6199
|
+
return Array.from(libs);
|
|
6200
|
+
}
|
|
6201
|
+
function buildCdnScripts(libs) {
|
|
6202
|
+
const tags = [];
|
|
6203
|
+
if (libs.includes("tailwind")) {
|
|
6204
|
+
tags.push(`<script>window.tailwind={config:{},silent:true}</script>`);
|
|
6205
|
+
tags.push(`<script src="https://cdn.tailwindcss.com"></script>`);
|
|
6206
|
+
}
|
|
6207
|
+
if (libs.includes("chartjs")) {
|
|
6208
|
+
tags.push(`<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>`);
|
|
6209
|
+
}
|
|
6210
|
+
if (libs.includes("anime")) {
|
|
6211
|
+
tags.push(`<script src="https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js"></script>`);
|
|
6212
|
+
}
|
|
6213
|
+
if (libs.includes("lucide")) {
|
|
6214
|
+
tags.push(`<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>`);
|
|
6215
|
+
}
|
|
6216
|
+
if (libs.includes("alpine")) {
|
|
6217
|
+
tags.push(`<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>`);
|
|
6218
|
+
}
|
|
6219
|
+
return tags.join("\n ");
|
|
6220
|
+
}
|
|
6221
|
+
function renderStandalone(template, slots, brand, style) {
|
|
6222
|
+
const bodyFn = HTML_BODY_FN[template.id];
|
|
6223
|
+
const body = bodyFn ? bodyFn(slots, template) : "";
|
|
6224
|
+
const allVars = { ...brandVars(brand), ...styleVars(style) };
|
|
6225
|
+
const inlineStyle = buildInlineStyle(allVars);
|
|
6226
|
+
const customCss = style.customCss ?? "";
|
|
6227
|
+
const override = style.templateOverrides?.[template.id]?.html;
|
|
6228
|
+
const finalBody = override ?? body;
|
|
6229
|
+
const libs = getRequiredLibs(template.id);
|
|
6230
|
+
const scripts = buildCdnScripts(libs);
|
|
6231
|
+
const w = template.defaultWidth ?? 600;
|
|
6232
|
+
const h = template.defaultHeight ?? 400;
|
|
6233
|
+
return `<!DOCTYPE html>
|
|
6234
|
+
<html lang="en">
|
|
6235
|
+
<head>
|
|
6236
|
+
<meta charset="UTF-8" />
|
|
6237
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6238
|
+
<title>${escHtml(template.name)} \u2014 forge0x2B</title>
|
|
6239
|
+
${scripts}
|
|
6240
|
+
<style>
|
|
6241
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
6242
|
+
html, body {
|
|
6243
|
+
margin: 0; padding: 0;
|
|
6244
|
+
width: ${w}px; height: ${h}px;
|
|
6245
|
+
overflow: hidden;
|
|
6246
|
+
background: transparent;
|
|
6247
|
+
display: flex; align-items: center; justify-content: center;
|
|
6248
|
+
}
|
|
6249
|
+
.pw-widget {
|
|
6250
|
+
font-family: var(--pw-font-body);
|
|
6251
|
+
color: var(--pw-brand-text);
|
|
6252
|
+
background: var(--pw-brand-surface);
|
|
6253
|
+
border-radius: var(--pw-radius-md);
|
|
6254
|
+
padding: var(--pw-spacing-md);
|
|
6255
|
+
border: var(--pw-border-width) solid rgba(255,255,255,.08);
|
|
6256
|
+
box-sizing: border-box;
|
|
6257
|
+
width: 100%; max-width: ${w}px;
|
|
6258
|
+
${customCss}
|
|
6259
|
+
}
|
|
6260
|
+
</style>
|
|
6261
|
+
</head>
|
|
6262
|
+
<body>
|
|
6263
|
+
<div class="pw-widget pw-${template.id}" style="${inlineStyle}">
|
|
6264
|
+
${finalBody}
|
|
6265
|
+
</div>
|
|
6266
|
+
</body>
|
|
6267
|
+
</html>`;
|
|
6268
|
+
}
|
|
2555
6269
|
function renderWidget(input) {
|
|
2556
6270
|
const { template, slots, brandKit, format } = input;
|
|
2557
6271
|
const style = !input.style ? STYLE_PRESET_DEFAULT : typeof input.style === "string" ? getStyleById(input.style) : input.style;
|
|
@@ -2562,6 +6276,9 @@ function renderWidget(input) {
|
|
|
2562
6276
|
if (format === "react") {
|
|
2563
6277
|
return renderReact(template, slots, brandKit ?? null, style);
|
|
2564
6278
|
}
|
|
6279
|
+
if (format === "standalone") {
|
|
6280
|
+
return renderStandalone(template, slots, brandKit ?? null, style);
|
|
6281
|
+
}
|
|
2565
6282
|
const bodyFn = HTML_BODY_FN[template.id];
|
|
2566
6283
|
const body = bodyFn ? bodyFn(slots, template) : "";
|
|
2567
6284
|
const allVars = { ...brandVars(brandKit ?? null), ...styleVars(style) };
|
|
@@ -2853,7 +6570,7 @@ function getDispatchChannel(id) {
|
|
|
2853
6570
|
}
|
|
2854
6571
|
|
|
2855
6572
|
// src/forge/dispatch.ts
|
|
2856
|
-
function
|
|
6573
|
+
function buildSystemPrompt17(channel, audience, brand, blueprintContext, toneOffset) {
|
|
2857
6574
|
const lines = [
|
|
2858
6575
|
`You are a skilled content writer producing a ${channel.name} post.`,
|
|
2859
6576
|
"",
|
|
@@ -2904,7 +6621,7 @@ function truncate(content, maxLength) {
|
|
|
2904
6621
|
async function generateForChannel(ask, channel, provider, opts) {
|
|
2905
6622
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2906
6623
|
try {
|
|
2907
|
-
const system =
|
|
6624
|
+
const system = buildSystemPrompt17(
|
|
2908
6625
|
channel,
|
|
2909
6626
|
opts.audience,
|
|
2910
6627
|
opts.brand,
|
|
@@ -3049,7 +6766,7 @@ function nextVersionNumber(versions) {
|
|
|
3049
6766
|
}
|
|
3050
6767
|
|
|
3051
6768
|
// src/forge/refineAsset.ts
|
|
3052
|
-
function
|
|
6769
|
+
function buildSystemPrompt18(input) {
|
|
3053
6770
|
const parts = [
|
|
3054
6771
|
`You are a content refinement assistant for a developer-focused content tool (forge0x2B).`,
|
|
3055
6772
|
`You are refining an existing ${input.assetType.replace(/-/g, " ")} asset.`,
|
|
@@ -3088,7 +6805,7 @@ function parseRefinedResponse(raw) {
|
|
|
3088
6805
|
return { newContent, llmReply };
|
|
3089
6806
|
}
|
|
3090
6807
|
async function refineAsset(input, provider) {
|
|
3091
|
-
const systemPrompt =
|
|
6808
|
+
const systemPrompt = buildSystemPrompt18(input);
|
|
3092
6809
|
if (scanForSecrets(systemPrompt)) {
|
|
3093
6810
|
throw new Error("refineAsset: secret pattern detected in asset content. Refusing to send to LLM.");
|
|
3094
6811
|
}
|
|
@@ -3250,4 +6967,4 @@ function previewExport(entries, format) {
|
|
|
3250
6967
|
}
|
|
3251
6968
|
}
|
|
3252
6969
|
|
|
3253
|
-
export { ANIMATION_DURATION_PRESETS, BRAND_CONTENT_SLOT_KEYS, BUNDLED_DISPATCH_CHANNELS, BUNDLED_STYLE_PRESETS, BUNDLED_WIDGET_TEMPLATES, DEFAULT_ANIMATION_DURATION_SECONDS, DEFAULT_BRAND_KIT_FONTS, DEFAULT_BRAND_KIT_PALETTE, DEFAULT_BRAND_KIT_VOICE, DISPATCH_CHANNEL_BLOG, DISPATCH_CHANNEL_EMAIL, DISPATCH_CHANNEL_HN, DISPATCH_CHANNEL_INSTAGRAM, DISPATCH_CHANNEL_LINKEDIN, DISPATCH_CHANNEL_NEWSLETTER, DISPATCH_CHANNEL_REDDIT, DISPATCH_CHANNEL_SLACK, DISPATCH_CHANNEL_TWEET, FORGE_AUDIENCES, FORGE_BRAND_THEME_ID, FORGE_TEMPLATES, FREE_TIER_WIDGET_IDS, MAX_ANIMATION_DURATION_SECONDS, MIN_ANIMATION_DURATION_SECONDS, PRISM_TEMPLATES, PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW, PRISM_TEMPLATE_REFACTOR_RATIONALE, PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT, PRISM_TEMPLATE_SHIPPING_DIGEST, PRISM_TEMPLATE_ZONE_DEEPDIVE, REFINE_COUNTDOWN_THRESHOLD, REFINE_SESSION_SOFT_CAP, REFINE_SUGGESTIONS, STYLE_PRESET_BRUTALIST, STYLE_PRESET_DEFAULT, STYLE_PRESET_GLASSY, STYLE_PRESET_MINIMAL, TEMPLATE_SCHEMA_EXAMPLES, TEMPLATE_SCHEMA_GENERIC, WIDGET_TEMPLATE_CHANGELOG_ROW, WIDGET_TEMPLATE_CTA_BANNER, WIDGET_TEMPLATE_FEATURE_GRID, WIDGET_TEMPLATE_METRIC_BADGE, WIDGET_TEMPLATE_PRICING_TIER, WIDGET_TEMPLATE_SOCIAL_PROOF, WIDGET_TEMPLATE_STAT_CARD, WIDGET_TEMPLATE_TESTIMONIAL, applyEntryPatch, asAudienceId, assembleBrandUrlExtractionPrompt, assembleForgePrompt, brandThemeConfigToEntry, buildPrismContextPrompt, buildRevertVersion, buildScheduledEntry, buildVersion, cascadingScheduledFor, clampAnimationDuration, computeDiff, defaultBrandKit, defaultValueForField, deriveContextSummary, distill, entryInRange, exportToBufferCsv, exportToHypefuryCsv, exportToICalendar, extractBrandFromPrismBlueprint, extractDependencyHotspots, extractTopChurnFiles, extractZones, generateArchitectureWalkthrough, generateAskDrivenAsset, generateChangesSince, generateOnboardingDoc, generateRefactoringReport, generateReleaseNotes, getDispatchChannel, getPrismTemplate, getSlotValue, getStyleById, getWidgetTemplate, initialFormValues, next7DaysRange, nextVersionNumber, orchestrateDispatch, parseAndValidateSchemaDefinition, parseBrandKitFromLlmResponse, parseBrandThemeContent, parseStyleFromCss, parseStyleFromTailwindConfig, parseStyleFromTokensJson, parseThemeConfigContent, previewExport, readBlueprintData, readBlueprintFromTarget, readPrismDirectory, refineAsset, refineLimitState, renderWidget, resolveAnimatedChoice, resolveAnimationDuration, resolveBrandPalette, runForgeGeneration, scanForSecrets, schemaExampleFor, schemaToForm, templateAnimatedDefault, themeEntryToPalette, tryParseJsonObject, validateAgainstTemplateSchema, validateAssetSlots, validateFormValues, validateSchemaDefinition };
|
|
6970
|
+
export { ANIMATION_DURATION_PRESETS, BRAND_CONTENT_SLOT_KEYS, BUNDLED_DISPATCH_CHANNELS, BUNDLED_STYLE_PRESETS, BUNDLED_WIDGET_TEMPLATES, DEFAULT_ANIMATION_DURATION_SECONDS, DEFAULT_BRAND_KIT_FONTS, DEFAULT_BRAND_KIT_PALETTE, DEFAULT_BRAND_KIT_VOICE, DISPATCH_CHANNEL_BLOG, DISPATCH_CHANNEL_EMAIL, DISPATCH_CHANNEL_HN, DISPATCH_CHANNEL_INSTAGRAM, DISPATCH_CHANNEL_LINKEDIN, DISPATCH_CHANNEL_NEWSLETTER, DISPATCH_CHANNEL_REDDIT, DISPATCH_CHANNEL_SLACK, DISPATCH_CHANNEL_TWEET, FORGE_AUDIENCES, FORGE_BRAND_THEME_ID, FORGE_TEMPLATES, FREE_TIER_WIDGET_IDS, MAX_ANIMATION_DURATION_SECONDS, MIN_ANIMATION_DURATION_SECONDS, PRISM_TEMPLATES, PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW, PRISM_TEMPLATE_REFACTOR_RATIONALE, PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT, PRISM_TEMPLATE_SHIPPING_DIGEST, PRISM_TEMPLATE_ZONE_DEEPDIVE, REFINE_COUNTDOWN_THRESHOLD, REFINE_SESSION_SOFT_CAP, REFINE_SUGGESTIONS, STYLE_PRESET_BRUTALIST, STYLE_PRESET_DEFAULT, STYLE_PRESET_GLASSY, STYLE_PRESET_MINIMAL, TEMPLATE_SCHEMA_EXAMPLES, TEMPLATE_SCHEMA_GENERIC, WIDGET_TEMPLATE_CHANGELOG_ROW, WIDGET_TEMPLATE_CTA_BANNER, WIDGET_TEMPLATE_FEATURE_GRID, WIDGET_TEMPLATE_METRIC_BADGE, WIDGET_TEMPLATE_PRICING_TIER, WIDGET_TEMPLATE_SOCIAL_PROOF, WIDGET_TEMPLATE_STAT_CARD, WIDGET_TEMPLATE_TESTIMONIAL, applyEntryPatch, asAudienceId, assembleBrandUrlExtractionPrompt, assembleForgePrompt, brandThemeConfigToEntry, buildPrismContextPrompt, buildRevertVersion, buildScheduledEntry, buildVersion, cascadingScheduledFor, clampAnimationDuration, computeDiff, defaultBrandKit, defaultValueForField, deriveContextSummary, distill, entryInRange, exportToBufferCsv, exportToHypefuryCsv, exportToICalendar, extractBrandFromPrismBlueprint, extractDependencyHotspots, extractTopChurnFiles, extractZones, generateADR, generateApiChangelog, generateArc42, generateArchitectureWalkthrough, generateAskDrivenAsset, generateChangeImpactBrief, generateChangesSince, generateComplianceDoc, generateKnowledgeCapture, generateNewsletter, generateOnboardingDoc, generatePresentation, generateRadio, generateRefactoringReport, generateReleaseNotes, generateRoiSlide, generateSprintRetro, getDispatchChannel, getPrismTemplate, getSlotValue, getStyleById, getWidgetTemplate, initialFormValues, next7DaysRange, nextVersionNumber, normalizePrismlensBlueprint, orchestrateDispatch, parseAndValidateSchemaDefinition, parseBrandKitFromLlmResponse, parseBrandThemeContent, parseStyleFromCss, parseStyleFromTailwindConfig, parseStyleFromTokensJson, parseThemeConfigContent, previewExport, readAmberLayer, readBlueprintData, readBlueprintFromTarget, readGreenLayer, readPrismDirectory, refineAsset, refineLimitState, renderWidget, resolveAnimatedChoice, resolveAnimationDuration, resolveBrandPalette, runForgeGeneration, scanForSecrets, schemaExampleFor, schemaToForm, templateAnimatedDefault, themeEntryToPalette, tryParseJsonObject, validateAgainstTemplateSchema, validateAssetSlots, validateFormValues, validateSchemaDefinition };
|