forgesmith 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2919 -402
- package/dist/index.d.cts +438 -48
- package/dist/index.d.ts +438 -48
- package/dist/index.mjs +2912 -389
- package/dist/server.cjs +6524 -0
- package/dist/server.d.cts +1387 -0
- package/dist/server.d.ts +1387 -0
- package/dist/server.mjs +6393 -0
- package/package.json +7 -2
package/dist/index.cjs
CHANGED
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var fs = require('fs/promises');
|
|
4
|
-
var path = require('path');
|
|
5
|
-
var fs$1 = require('fs');
|
|
6
3
|
var crypto = require('crypto');
|
|
7
4
|
|
|
8
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
-
|
|
10
|
-
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
11
|
-
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
12
|
-
|
|
13
5
|
// src/generators/releaseNotes.ts
|
|
14
6
|
function buildSystemPrompt() {
|
|
15
7
|
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.`;
|
|
16
8
|
}
|
|
17
|
-
function buildUserPrompt(data, opts) {
|
|
9
|
+
function buildUserPrompt(data, opts, amberContext, greenContext) {
|
|
18
10
|
const tone = opts.tone ?? "professional";
|
|
19
11
|
const length = opts.length ?? "medium";
|
|
20
12
|
const format = opts.format ?? "markdown";
|
|
21
13
|
const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
|
|
22
|
-
const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
|
|
14
|
+
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";
|
|
23
15
|
const hasSessions = (data.sessions?.length ?? 0) > 0;
|
|
24
16
|
const hasRecs = (data.recommendations?.length ?? 0) > 0;
|
|
25
17
|
const hasInsights = (data.insights?.length ?? 0) > 0;
|
|
@@ -55,11 +47,38 @@ function buildUserPrompt(data, opts) {
|
|
|
55
47
|
}
|
|
56
48
|
lines.push(``);
|
|
57
49
|
}
|
|
50
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
51
|
+
lines.push(`## Business Capabilities (AMBER layer)`);
|
|
52
|
+
lines.push(amberContext.summary);
|
|
53
|
+
const criticalCaps = amberContext.capabilities.filter(
|
|
54
|
+
(c) => c.criticality === "critical" || c.criticality === "high"
|
|
55
|
+
);
|
|
56
|
+
if (criticalCaps.length > 0) {
|
|
57
|
+
lines.push(
|
|
58
|
+
`This release may touch these capabilities: ${criticalCaps.map((c) => c.name).join(", ")}.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
lines.push(``);
|
|
62
|
+
}
|
|
63
|
+
if (greenContext) {
|
|
64
|
+
if (greenContext.coherenceScore !== null) {
|
|
65
|
+
lines.push(`## Architecture Health (GREEN layer)`);
|
|
66
|
+
lines.push(
|
|
67
|
+
`Coherence score: ${greenContext.coherenceScore}/100 (grade ${greenContext.coherenceGrade ?? "?"}).`
|
|
68
|
+
);
|
|
69
|
+
if (greenContext.topRisks.length > 0) {
|
|
70
|
+
lines.push(
|
|
71
|
+
`Top risks: ${greenContext.topRisks.slice(0, 3).map((r) => `${r.name} \u2014 ${r.reason}`).join("; ")}.`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
lines.push(``);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
58
77
|
return lines.join("\n");
|
|
59
78
|
}
|
|
60
|
-
async function generateReleaseNotes(data, opts, provider) {
|
|
79
|
+
async function generateReleaseNotes(data, opts, provider, amberContext, greenContext) {
|
|
61
80
|
const systemPrompt = buildSystemPrompt();
|
|
62
|
-
const userPrompt = buildUserPrompt(data, opts);
|
|
81
|
+
const userPrompt = buildUserPrompt(data, opts, amberContext, greenContext);
|
|
63
82
|
const response = await provider.complete({
|
|
64
83
|
systemPrompt,
|
|
65
84
|
messages: [{ role: "user", content: userPrompt }],
|
|
@@ -79,13 +98,16 @@ async function generateReleaseNotes(data, opts, provider) {
|
|
|
79
98
|
var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
|
|
80
99
|
function buildSystemPrompt2(tone) {
|
|
81
100
|
const toneMap = {
|
|
82
|
-
|
|
101
|
+
professional: "You are a senior software architect who writes clear, professional documentation suitable for all engineering audiences.",
|
|
83
102
|
casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
|
|
103
|
+
technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
|
|
104
|
+
executive: "You are a CTO-level advisor who explains codebase architecture at a high strategic level for leadership audiences.",
|
|
105
|
+
// Legacy aliases kept for backwards compatibility
|
|
84
106
|
onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
|
|
85
107
|
};
|
|
86
|
-
return `${toneMap[tone] ?? toneMap.
|
|
108
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the requested document \u2014 no preamble, no meta-commentary.`;
|
|
87
109
|
}
|
|
88
|
-
function buildUserPrompt2(blueprint, opts) {
|
|
110
|
+
function buildUserPrompt2(blueprint, opts, amberContext) {
|
|
89
111
|
const tone = opts.tone ?? "technical";
|
|
90
112
|
const length = opts.length ?? "long";
|
|
91
113
|
const format = opts.format ?? "markdown";
|
|
@@ -113,9 +135,28 @@ function buildUserPrompt2(blueprint, opts) {
|
|
|
113
135
|
``,
|
|
114
136
|
`Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
|
|
115
137
|
];
|
|
138
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
139
|
+
lines.push(``);
|
|
140
|
+
lines.push(`## Business Capabilities (AMBER layer)`);
|
|
141
|
+
lines.push(amberContext.summary);
|
|
142
|
+
lines.push(``);
|
|
143
|
+
const sorted = amberContext.capabilities.slice().sort((a, b) => {
|
|
144
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
145
|
+
return (rank[a.criticality] ?? 4) - (rank[b.criticality] ?? 4);
|
|
146
|
+
});
|
|
147
|
+
for (const cap of sorted) {
|
|
148
|
+
const tags = [cap.criticality, cap.lifecycle].filter(Boolean).join(", ");
|
|
149
|
+
const fileCount = cap.files.length;
|
|
150
|
+
lines.push(
|
|
151
|
+
`- **${cap.name}** (${cap.id}) [${tags}]${fileCount > 0 ? ` \u2014 ${fileCount} file${fileCount === 1 ? "" : "s"}` : ""}`
|
|
152
|
+
);
|
|
153
|
+
if (cap.description) lines.push(` ${cap.description}`);
|
|
154
|
+
}
|
|
155
|
+
lines.push(`Include a "Business Capabilities" section in the walkthrough mapping these capabilities to the codebase.`);
|
|
156
|
+
}
|
|
116
157
|
return lines.join("\n");
|
|
117
158
|
}
|
|
118
|
-
async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
159
|
+
async function generateArchitectureWalkthrough(blueprint, opts, provider, amberContext) {
|
|
119
160
|
if (!blueprint) {
|
|
120
161
|
return {
|
|
121
162
|
text: NO_DATA_MSG,
|
|
@@ -124,7 +165,7 @@ async function generateArchitectureWalkthrough(blueprint, opts, provider) {
|
|
|
124
165
|
}
|
|
125
166
|
const response = await provider.complete({
|
|
126
167
|
systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
|
|
127
|
-
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
|
|
168
|
+
messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts, amberContext) }],
|
|
128
169
|
maxTokens: 4096
|
|
129
170
|
});
|
|
130
171
|
return {
|
|
@@ -199,11 +240,15 @@ async function generateChangesSince(current, previous, opts, provider) {
|
|
|
199
240
|
var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
|
|
200
241
|
function buildSystemPrompt4(tone) {
|
|
201
242
|
const toneMap = {
|
|
243
|
+
professional: "You are a documentation engineer writing a structured, professional onboarding reference for engineering teams.",
|
|
244
|
+
casual: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
245
|
+
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals.",
|
|
246
|
+
executive: "You are a technical lead writing a concise onboarding overview suitable for both technical and leadership audiences.",
|
|
247
|
+
// Legacy aliases kept for backwards compatibility
|
|
202
248
|
friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
|
|
203
|
-
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams."
|
|
204
|
-
technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
|
|
249
|
+
formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams."
|
|
205
250
|
};
|
|
206
|
-
return `${toneMap[tone] ?? toneMap.
|
|
251
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the document \u2014 no preamble, no meta-commentary.`;
|
|
207
252
|
}
|
|
208
253
|
function buildUserPrompt4(blueprint, opts) {
|
|
209
254
|
const tone = opts.tone ?? "friendly";
|
|
@@ -264,11 +309,14 @@ async function generateOnboardingDoc(blueprint, opts, provider) {
|
|
|
264
309
|
var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
|
|
265
310
|
function buildSystemPrompt5(tone) {
|
|
266
311
|
const toneMap = {
|
|
267
|
-
|
|
312
|
+
professional: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
|
|
268
313
|
casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
|
|
269
|
-
|
|
314
|
+
technical: "You are a senior engineer who produces detailed, implementation-focused refactoring analysis with concrete code-level recommendations.",
|
|
315
|
+
executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership.",
|
|
316
|
+
// Legacy aliases kept for backwards compatibility
|
|
317
|
+
analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization."
|
|
270
318
|
};
|
|
271
|
-
return `${toneMap[tone] ?? toneMap.
|
|
319
|
+
return `${toneMap[tone] ?? toneMap.professional} Write only the report \u2014 no preamble, no meta-commentary.`;
|
|
272
320
|
}
|
|
273
321
|
function detectImportCycles(edges) {
|
|
274
322
|
const graph = /* @__PURE__ */ new Map();
|
|
@@ -279,12 +327,12 @@ function detectImportCycles(edges) {
|
|
|
279
327
|
const cycles = [];
|
|
280
328
|
const visited = /* @__PURE__ */ new Set();
|
|
281
329
|
const stack = /* @__PURE__ */ new Set();
|
|
282
|
-
function dfs(node,
|
|
330
|
+
function dfs(node, path) {
|
|
283
331
|
if (cycles.length >= 5) return;
|
|
284
332
|
if (stack.has(node)) {
|
|
285
|
-
const cycleStart =
|
|
333
|
+
const cycleStart = path.indexOf(node);
|
|
286
334
|
if (cycleStart !== -1) {
|
|
287
|
-
cycles.push(
|
|
335
|
+
cycles.push(path.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
|
|
288
336
|
}
|
|
289
337
|
return;
|
|
290
338
|
}
|
|
@@ -292,7 +340,7 @@ function detectImportCycles(edges) {
|
|
|
292
340
|
visited.add(node);
|
|
293
341
|
stack.add(node);
|
|
294
342
|
for (const neighbor of graph.get(node) ?? []) {
|
|
295
|
-
dfs(neighbor, [...
|
|
343
|
+
dfs(neighbor, [...path, node]);
|
|
296
344
|
}
|
|
297
345
|
stack.delete(node);
|
|
298
346
|
}
|
|
@@ -301,7 +349,7 @@ function detectImportCycles(edges) {
|
|
|
301
349
|
}
|
|
302
350
|
return cycles;
|
|
303
351
|
}
|
|
304
|
-
function buildUserPrompt5(blueprint, opts) {
|
|
352
|
+
function buildUserPrompt5(blueprint, opts, amberContext, greenContext) {
|
|
305
353
|
const tone = opts.tone ?? "analytical";
|
|
306
354
|
const length = opts.length ?? "medium";
|
|
307
355
|
const format = opts.format ?? "markdown";
|
|
@@ -328,9 +376,42 @@ function buildUserPrompt5(blueprint, opts) {
|
|
|
328
376
|
``,
|
|
329
377
|
`Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
|
|
330
378
|
];
|
|
379
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
380
|
+
lines.push(``);
|
|
381
|
+
lines.push(`## Business Capabilities with Documentation Drift (AMBER layer)`);
|
|
382
|
+
lines.push(amberContext.summary);
|
|
383
|
+
const drifted = amberContext.capabilities.filter((c) => c.driftCount > 0);
|
|
384
|
+
if (drifted.length > 0) {
|
|
385
|
+
lines.push(`Capabilities with drift (files changed without updating @amber-doc):`);
|
|
386
|
+
for (const cap of drifted) {
|
|
387
|
+
lines.push(
|
|
388
|
+
`- **${cap.name}** (${cap.id}, ${cap.criticality}): ${cap.driftCount} file${cap.driftCount === 1 ? "" : "s"} drifted`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (greenContext) {
|
|
394
|
+
lines.push(``);
|
|
395
|
+
lines.push(`## Cross-Layer Risks (GREEN layer)`);
|
|
396
|
+
lines.push(greenContext.summary);
|
|
397
|
+
if (greenContext.topRisks.length > 0) {
|
|
398
|
+
lines.push(`Top architectural risks:`);
|
|
399
|
+
for (const risk of greenContext.topRisks) {
|
|
400
|
+
lines.push(
|
|
401
|
+
`- **${risk.name}** (impact: ${risk.impact}): ${risk.reason}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (greenContext.insights.length > 0) {
|
|
406
|
+
lines.push(`Cross-layer insights:`);
|
|
407
|
+
for (const insight of greenContext.insights.slice(0, 5)) {
|
|
408
|
+
lines.push(`- **${insight.title}**: ${insight.body}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
331
412
|
return lines.join("\n");
|
|
332
413
|
}
|
|
333
|
-
async function generateRefactoringReport(blueprint, opts, provider) {
|
|
414
|
+
async function generateRefactoringReport(blueprint, opts, provider, amberContext, greenContext) {
|
|
334
415
|
if (!blueprint) {
|
|
335
416
|
return {
|
|
336
417
|
text: NO_DATA_MSG4,
|
|
@@ -339,7 +420,7 @@ async function generateRefactoringReport(blueprint, opts, provider) {
|
|
|
339
420
|
}
|
|
340
421
|
const response = await provider.complete({
|
|
341
422
|
systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
|
|
342
|
-
messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts) }],
|
|
423
|
+
messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts, amberContext, greenContext) }],
|
|
343
424
|
maxTokens: 3072
|
|
344
425
|
});
|
|
345
426
|
return {
|
|
@@ -347,43 +428,6 @@ async function generateRefactoringReport(blueprint, opts, provider) {
|
|
|
347
428
|
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
348
429
|
};
|
|
349
430
|
}
|
|
350
|
-
async function readJsonFiles(dir) {
|
|
351
|
-
try {
|
|
352
|
-
const entries = await fs__default.default.readdir(dir);
|
|
353
|
-
const results = [];
|
|
354
|
-
for (const entry of entries) {
|
|
355
|
-
if (!entry.endsWith(".json")) continue;
|
|
356
|
-
try {
|
|
357
|
-
const raw = await fs__default.default.readFile(path__default.default.join(dir, entry), "utf-8");
|
|
358
|
-
results.push(JSON.parse(raw));
|
|
359
|
-
} catch {
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
return results;
|
|
363
|
-
} catch {
|
|
364
|
-
return [];
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
async function readPrismDirectory(prismPath) {
|
|
368
|
-
const sessionsDir = path__default.default.join(prismPath, "sessions");
|
|
369
|
-
const recsDir = path__default.default.join(prismPath, "recommendations");
|
|
370
|
-
const insightsDir = path__default.default.join(prismPath, "green", "insights", "accepted");
|
|
371
|
-
const [sessions, recommendations, insights] = await Promise.all([
|
|
372
|
-
readJsonFiles(sessionsDir),
|
|
373
|
-
readJsonFiles(recsDir),
|
|
374
|
-
readJsonFiles(insightsDir)
|
|
375
|
-
]);
|
|
376
|
-
return { sessions, recommendations, insights };
|
|
377
|
-
}
|
|
378
|
-
async function readBlueprintData(targetPath) {
|
|
379
|
-
const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
380
|
-
try {
|
|
381
|
-
const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
|
|
382
|
-
return JSON.parse(raw);
|
|
383
|
-
} catch {
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
431
|
|
|
388
432
|
// src/generators/askDrivenAsset.ts
|
|
389
433
|
var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
|
|
@@ -402,69 +446,2402 @@ function buildSystemPrompt6(format, tone) {
|
|
|
402
446
|
technical: "You are a senior software engineer writing precise, implementation-focused content.",
|
|
403
447
|
executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
|
|
404
448
|
};
|
|
405
|
-
const guide = FORMAT_GUIDES[format];
|
|
406
|
-
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.`;
|
|
449
|
+
const guide = FORMAT_GUIDES[format];
|
|
450
|
+
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.`;
|
|
451
|
+
}
|
|
452
|
+
function buildUserPrompt6(blueprint, question, opts) {
|
|
453
|
+
const format = opts.format ?? "markdown";
|
|
454
|
+
const tone = opts.tone ?? "professional";
|
|
455
|
+
const length = opts.length ?? "medium";
|
|
456
|
+
const guide = FORMAT_GUIDES[format];
|
|
457
|
+
const lengthGuide = {
|
|
458
|
+
short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
|
|
459
|
+
medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
|
|
460
|
+
long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
|
|
461
|
+
}[length];
|
|
462
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
463
|
+
const lines = [
|
|
464
|
+
`## User's Question`,
|
|
465
|
+
question,
|
|
466
|
+
``,
|
|
467
|
+
`## Output Requirements`,
|
|
468
|
+
`- Format: ${guide.name}`,
|
|
469
|
+
`- Tone: ${tone}`,
|
|
470
|
+
`- Length: ${length} (${lengthGuide})`,
|
|
471
|
+
``,
|
|
472
|
+
`## Codebase Architecture Context`,
|
|
473
|
+
`Target: ${blueprint.targetPath}`,
|
|
474
|
+
`Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
475
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
476
|
+
``,
|
|
477
|
+
`Key files (by usage):`,
|
|
478
|
+
...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
|
|
479
|
+
];
|
|
480
|
+
if (blueprint.edges.length > 0) {
|
|
481
|
+
const edgeSample = blueprint.edges.slice(0, 15);
|
|
482
|
+
lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
|
|
483
|
+
}
|
|
484
|
+
lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
|
|
485
|
+
return lines.join("\n");
|
|
486
|
+
}
|
|
487
|
+
async function generateAskDrivenAsset(blueprint, question, opts, provider) {
|
|
488
|
+
if (!blueprint) {
|
|
489
|
+
return {
|
|
490
|
+
text: NO_DATA_MSG5,
|
|
491
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (!question || !question.trim()) {
|
|
495
|
+
return {
|
|
496
|
+
text: "No question provided. Please ask something about your codebase.",
|
|
497
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
const format = opts.format ?? "markdown";
|
|
501
|
+
const tone = opts.tone ?? "professional";
|
|
502
|
+
const maxTokens = FORMAT_GUIDES[format].maxTokens;
|
|
503
|
+
const response = await provider.complete({
|
|
504
|
+
systemPrompt: buildSystemPrompt6(format, tone),
|
|
505
|
+
messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
|
|
506
|
+
maxTokens
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
text: response.content,
|
|
510
|
+
metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/generators/generatePresentation.ts
|
|
515
|
+
function buildContext(blueprint, opts) {
|
|
516
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
|
|
517
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
518
|
+
const totalLoc = blueprint.files.reduce(
|
|
519
|
+
(sum, f) => sum + (f.lineCount ?? 0),
|
|
520
|
+
0
|
|
521
|
+
);
|
|
522
|
+
const lines = [
|
|
523
|
+
`## Codebase Context`,
|
|
524
|
+
`Project: ${projectName}`,
|
|
525
|
+
`Path: ${blueprint.targetPath}`,
|
|
526
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
527
|
+
`Total lines of code: ${totalLoc}`,
|
|
528
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
529
|
+
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
530
|
+
``,
|
|
531
|
+
`## Most-imported files`,
|
|
532
|
+
...topFiles.map(
|
|
533
|
+
(f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, lines: ${f.lineCount ?? "?"}`
|
|
534
|
+
)
|
|
535
|
+
];
|
|
536
|
+
if (blueprint.edges.length > 0) {
|
|
537
|
+
const edgeSample = blueprint.edges.slice(0, 10);
|
|
538
|
+
lines.push(
|
|
539
|
+
``,
|
|
540
|
+
`## Dependency sample`,
|
|
541
|
+
...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
return lines.join("\n");
|
|
545
|
+
}
|
|
546
|
+
function planSlideTypes(opts) {
|
|
547
|
+
const { slideCount, focusArea, audience } = opts;
|
|
548
|
+
const focus = focusArea ?? "all";
|
|
549
|
+
const always = ["title", "closing"];
|
|
550
|
+
let middle = [];
|
|
551
|
+
if (focus === "architecture") {
|
|
552
|
+
middle = ["executive", "architecture", "metrics", "risks", "recommendations"];
|
|
553
|
+
} else if (focus === "health") {
|
|
554
|
+
middle = ["executive", "health", "risks", "recommendations", "metrics"];
|
|
555
|
+
} else if (focus === "risks") {
|
|
556
|
+
middle = ["executive", "risks", "recommendations", "architecture", "health"];
|
|
557
|
+
} else {
|
|
558
|
+
if (audience === "executive") {
|
|
559
|
+
middle = ["executive", "health", "risks", "recommendations", "architecture", "metrics", "capabilities", "recommendations"];
|
|
560
|
+
} else if (audience === "technical") {
|
|
561
|
+
middle = ["architecture", "metrics", "risks", "health", "recommendations", "capabilities", "executive", "recommendations"];
|
|
562
|
+
} else {
|
|
563
|
+
middle = ["executive", "architecture", "health", "risks", "recommendations", "metrics", "capabilities", "executive"];
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const targetMiddle = slideCount - 2;
|
|
567
|
+
const trimmed = middle.slice(0, targetMiddle);
|
|
568
|
+
const seen = /* @__PURE__ */ new Set();
|
|
569
|
+
const deduped = [];
|
|
570
|
+
for (const t of trimmed) {
|
|
571
|
+
if (!seen.has(t)) {
|
|
572
|
+
seen.add(t);
|
|
573
|
+
deduped.push(t);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return [always[0], ...deduped, always[1]];
|
|
577
|
+
}
|
|
578
|
+
function buildSystemPrompt7(opts) {
|
|
579
|
+
const toneMap = {
|
|
580
|
+
professional: "You are a senior engineering consultant who creates compelling, data-driven presentations for technical and business stakeholders.",
|
|
581
|
+
casual: "You are a developer advocate who creates friendly, approachable presentations that make architecture exciting.",
|
|
582
|
+
executive: "You are a VP of Engineering who creates concise, business-value-focused presentations for C-suite audiences."
|
|
583
|
+
};
|
|
584
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
585
|
+
}
|
|
586
|
+
function buildUserPrompt7(blueprint, opts, slideTypes) {
|
|
587
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
588
|
+
const context = buildContext(blueprint, opts);
|
|
589
|
+
const slideTypeDescriptions = {
|
|
590
|
+
title: "Opening slide with project name, tagline, and date. Fields: title (project name), subtitle (tagline), speakerNotes",
|
|
591
|
+
executive: "Executive summary with 3 key findings. Fields: title, bullets (exactly 3 bullet strings), speakerNotes",
|
|
592
|
+
architecture: "Architecture overview. Fields: title, subtitle (layer summary), bullets (up to 5 key architecture facts), visualHint (describe a diagram), speakerNotes",
|
|
593
|
+
capabilities: "Capability map overview. Fields: title, bullets (up to 5 capability areas found), speakerNotes",
|
|
594
|
+
health: "Codebase health score. Fields: title, highlight (a letter grade A-F or numeric score), highlightLabel, bullets (up to 3 health indicators), speakerNotes",
|
|
595
|
+
risks: "Top risks. Fields: title, bullets (exactly 3 risks, each prefixed with High/Medium/Low impact), speakerNotes",
|
|
596
|
+
recommendations: "Action items. Fields: title, bullets (up to 5 concrete action items), speakerNotes",
|
|
597
|
+
metrics: "Key metrics. Fields: title, highlight (most important metric), highlightLabel, bullets (up to 5 metrics), speakerNotes",
|
|
598
|
+
closing: "Next steps and call to action. Fields: title, subtitle, bullets (up to 3 next steps), speakerNotes"
|
|
599
|
+
};
|
|
600
|
+
const slidePlan = slideTypes.map((t, i) => ` ${i + 1}. type="${t}" \u2014 ${slideTypeDescriptions[t]}`).join("\n");
|
|
601
|
+
return [
|
|
602
|
+
`Generate a ${opts.slideCount}-slide presentation deck for: ${projectName}`,
|
|
603
|
+
`Audience: ${opts.audience}. Tone: ${opts.tone}. Theme: ${opts.theme}.`,
|
|
604
|
+
``,
|
|
605
|
+
context,
|
|
606
|
+
``,
|
|
607
|
+
`## Slide Plan (${slideTypes.length} slides)`,
|
|
608
|
+
slidePlan,
|
|
609
|
+
``,
|
|
610
|
+
`## Output Format`,
|
|
611
|
+
`Return a single JSON object with this exact shape:`,
|
|
612
|
+
`{`,
|
|
613
|
+
` "title": "<deck title>",`,
|
|
614
|
+
` "subtitle": "<deck subtitle>",`,
|
|
615
|
+
` "slides": [`,
|
|
616
|
+
` {`,
|
|
617
|
+
` "type": "<one of the PresentationSlideType values>",`,
|
|
618
|
+
` "title": "<slide title>",`,
|
|
619
|
+
` "subtitle": "<optional subtitle>",`,
|
|
620
|
+
` "bullets": ["<bullet 1>", "<bullet 2>"],`,
|
|
621
|
+
` "highlight": "<optional large callout>",`,
|
|
622
|
+
` "highlightLabel": "<optional label>",`,
|
|
623
|
+
` "speakerNotes": "<presenter notes>",`,
|
|
624
|
+
` "visualHint": "<optional visual description>"`,
|
|
625
|
+
` }`,
|
|
626
|
+
` ]`,
|
|
627
|
+
`}`,
|
|
628
|
+
``,
|
|
629
|
+
`Rules:`,
|
|
630
|
+
`- Each slide in the output MUST match the type listed in the slide plan above.`,
|
|
631
|
+
`- bullets array: max 5 items, each \u2264 80 characters.`,
|
|
632
|
+
`- speakerNotes: 1-3 sentences a presenter would say.`,
|
|
633
|
+
`- Derive all content from the codebase context above \u2014 no invented facts.`,
|
|
634
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
635
|
+
].join("\n");
|
|
636
|
+
}
|
|
637
|
+
function buildFallback(blueprint, opts, slideTypes) {
|
|
638
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
639
|
+
const totalLoc = blueprint.files.reduce(
|
|
640
|
+
(sum, f) => sum + (f.lineCount ?? 0),
|
|
641
|
+
0
|
|
642
|
+
);
|
|
643
|
+
const slideBuilders = {
|
|
644
|
+
title: () => ({
|
|
645
|
+
type: "title",
|
|
646
|
+
title: projectName,
|
|
647
|
+
subtitle: "Architecture & Code Intelligence Report",
|
|
648
|
+
speakerNotes: `Welcome. Today we cover the architecture of ${projectName}.`
|
|
649
|
+
}),
|
|
650
|
+
executive: () => ({
|
|
651
|
+
type: "executive",
|
|
652
|
+
title: "Executive Summary",
|
|
653
|
+
bullets: [
|
|
654
|
+
`${blueprint.stats.totalFiles} files analyzed across all layers`,
|
|
655
|
+
`${blueprint.stats.runtimeEdges} dependency edges mapped`,
|
|
656
|
+
`Architecture categories: app, components, lib, hooks`
|
|
657
|
+
],
|
|
658
|
+
speakerNotes: "Three key findings from the codebase scan."
|
|
659
|
+
}),
|
|
660
|
+
architecture: () => ({
|
|
661
|
+
type: "architecture",
|
|
662
|
+
title: "Architecture Overview",
|
|
663
|
+
subtitle: `${blueprint.stats.totalFiles} files, ${blueprint.stats.runtimeEdges} edges`,
|
|
664
|
+
bullets: [
|
|
665
|
+
`App layer: ${blueprint.categories.app ?? 0} files`,
|
|
666
|
+
`Components: ${blueprint.categories.component ?? 0} files`,
|
|
667
|
+
`Libraries: ${blueprint.categories.lib ?? 0} files`,
|
|
668
|
+
`Hooks: ${blueprint.categories.hook ?? 0} files`
|
|
669
|
+
],
|
|
670
|
+
visualHint: "Layered architecture diagram with dependency arrows",
|
|
671
|
+
speakerNotes: "The codebase is organized into four primary layers."
|
|
672
|
+
}),
|
|
673
|
+
capabilities: () => ({
|
|
674
|
+
type: "capabilities",
|
|
675
|
+
title: "Capability Map",
|
|
676
|
+
bullets: ["Core application logic", "Component layer", "Shared utilities", "Custom hooks"],
|
|
677
|
+
speakerNotes: "These are the key capability areas identified in the scan."
|
|
678
|
+
}),
|
|
679
|
+
health: () => ({
|
|
680
|
+
type: "health",
|
|
681
|
+
title: "Codebase Health",
|
|
682
|
+
highlight: "B",
|
|
683
|
+
highlightLabel: "Overall Grade",
|
|
684
|
+
bullets: [
|
|
685
|
+
`${blueprint.stats.totalFiles} total files`,
|
|
686
|
+
`${blueprint.stats.runtimeEdges} dependency edges`,
|
|
687
|
+
`Avg LOC per file: ${blueprint.stats.totalFiles > 0 ? Math.round(totalLoc / blueprint.stats.totalFiles) : 0}`
|
|
688
|
+
],
|
|
689
|
+
speakerNotes: "The codebase shows good structural health with opportunities for improvement."
|
|
690
|
+
}),
|
|
691
|
+
risks: () => ({
|
|
692
|
+
type: "risks",
|
|
693
|
+
title: "Top Risks",
|
|
694
|
+
bullets: [
|
|
695
|
+
"High impact: Large files with many incoming dependencies",
|
|
696
|
+
"Medium impact: Missing test coverage in critical paths",
|
|
697
|
+
"Low impact: Inconsistent category classification"
|
|
698
|
+
],
|
|
699
|
+
speakerNotes: "Three risks identified, prioritized by impact."
|
|
700
|
+
}),
|
|
701
|
+
recommendations: () => ({
|
|
702
|
+
type: "recommendations",
|
|
703
|
+
title: "Recommendations",
|
|
704
|
+
bullets: [
|
|
705
|
+
"Break down files with high importedByCount into smaller modules",
|
|
706
|
+
"Establish clear boundaries between app, lib, and component layers",
|
|
707
|
+
"Add test coverage to files with the most incoming dependencies",
|
|
708
|
+
"Review and resolve circular dependency patterns",
|
|
709
|
+
"Document architecture decisions for new team members"
|
|
710
|
+
],
|
|
711
|
+
speakerNotes: "Five concrete action items, ordered by priority."
|
|
712
|
+
}),
|
|
713
|
+
metrics: () => ({
|
|
714
|
+
type: "metrics",
|
|
715
|
+
title: "Key Metrics",
|
|
716
|
+
highlight: String(blueprint.stats.totalFiles),
|
|
717
|
+
highlightLabel: "Total Files",
|
|
718
|
+
bullets: [
|
|
719
|
+
`Lines of code: ${totalLoc.toLocaleString()}`,
|
|
720
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
721
|
+
`App files: ${blueprint.categories.app ?? 0}`,
|
|
722
|
+
`Components: ${blueprint.categories.component ?? 0}`,
|
|
723
|
+
`Libraries: ${blueprint.categories.lib ?? 0}`
|
|
724
|
+
],
|
|
725
|
+
speakerNotes: "Key quantitative metrics from the Blueprint scan."
|
|
726
|
+
}),
|
|
727
|
+
closing: () => ({
|
|
728
|
+
type: "closing",
|
|
729
|
+
title: "Next Steps",
|
|
730
|
+
subtitle: "Start with the highest-impact recommendations",
|
|
731
|
+
bullets: [
|
|
732
|
+
"Schedule architecture review with the team",
|
|
733
|
+
"Prioritize the top 3 recommendations",
|
|
734
|
+
"Set up regular prism0x2A scans"
|
|
735
|
+
],
|
|
736
|
+
speakerNotes: "Thank you. Questions?"
|
|
737
|
+
})
|
|
738
|
+
};
|
|
739
|
+
const slides = slideTypes.map((t) => slideBuilders[t]());
|
|
740
|
+
return {
|
|
741
|
+
title: `${projectName} \u2014 Architecture Deck`,
|
|
742
|
+
subtitle: `${opts.audience.charAt(0).toUpperCase() + opts.audience.slice(1)} presentation \xB7 ${(/* @__PURE__ */ new Date()).toLocaleDateString()}`,
|
|
743
|
+
slideCount: slides.length,
|
|
744
|
+
slides,
|
|
745
|
+
theme: opts.theme,
|
|
746
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function parseLlmResponse(raw, slideTypes, opts, blueprint) {
|
|
750
|
+
let text = raw.trim();
|
|
751
|
+
if (text.startsWith("```")) {
|
|
752
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
753
|
+
}
|
|
754
|
+
let parsed;
|
|
755
|
+
try {
|
|
756
|
+
parsed = JSON.parse(text);
|
|
757
|
+
} catch {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
const rawSlides = Array.isArray(parsed.slides) ? parsed.slides : [];
|
|
761
|
+
if (rawSlides.length === 0) return null;
|
|
762
|
+
const slides = rawSlides.map((s, i) => {
|
|
763
|
+
const slide = s ?? {};
|
|
764
|
+
const inferredType = typeof slide.type === "string" && [
|
|
765
|
+
"title",
|
|
766
|
+
"executive",
|
|
767
|
+
"architecture",
|
|
768
|
+
"capabilities",
|
|
769
|
+
"health",
|
|
770
|
+
"risks",
|
|
771
|
+
"recommendations",
|
|
772
|
+
"metrics",
|
|
773
|
+
"closing"
|
|
774
|
+
].includes(slide.type) ? slide.type : slideTypes[i] ?? "executive";
|
|
775
|
+
const bullets = Array.isArray(slide.bullets) ? slide.bullets.filter((b) => typeof b === "string").slice(0, 5) : void 0;
|
|
776
|
+
return {
|
|
777
|
+
type: inferredType,
|
|
778
|
+
title: typeof slide.title === "string" ? slide.title : `Slide ${i + 1}`,
|
|
779
|
+
...typeof slide.subtitle === "string" && slide.subtitle ? { subtitle: slide.subtitle } : {},
|
|
780
|
+
...bullets && bullets.length > 0 ? { bullets } : {},
|
|
781
|
+
...typeof slide.highlight === "string" && slide.highlight ? { highlight: slide.highlight } : {},
|
|
782
|
+
...typeof slide.highlightLabel === "string" && slide.highlightLabel ? { highlightLabel: slide.highlightLabel } : {},
|
|
783
|
+
...typeof slide.speakerNotes === "string" && slide.speakerNotes ? { speakerNotes: slide.speakerNotes } : {},
|
|
784
|
+
...typeof slide.visualHint === "string" && slide.visualHint ? { visualHint: slide.visualHint } : {}
|
|
785
|
+
};
|
|
786
|
+
});
|
|
787
|
+
const projectName = opts.projectName || blueprint.targetPath.split("/").filter(Boolean).pop() || "Project";
|
|
788
|
+
return {
|
|
789
|
+
title: typeof parsed.title === "string" && parsed.title ? parsed.title : `${projectName} \u2014 Architecture Deck`,
|
|
790
|
+
subtitle: typeof parsed.subtitle === "string" && parsed.subtitle ? parsed.subtitle : `${opts.audience} presentation`,
|
|
791
|
+
slideCount: slides.length,
|
|
792
|
+
slides,
|
|
793
|
+
theme: opts.theme,
|
|
794
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
async function generatePresentation(opts) {
|
|
798
|
+
const { blueprint, llm } = opts;
|
|
799
|
+
const slideTypes = planSlideTypes(opts);
|
|
800
|
+
try {
|
|
801
|
+
const response = await llm.complete({
|
|
802
|
+
systemPrompt: buildSystemPrompt7(opts),
|
|
803
|
+
messages: [
|
|
804
|
+
{
|
|
805
|
+
role: "user",
|
|
806
|
+
content: buildUserPrompt7(blueprint, opts, slideTypes)
|
|
807
|
+
}
|
|
808
|
+
],
|
|
809
|
+
maxTokens: 4096
|
|
810
|
+
});
|
|
811
|
+
const parsed = parseLlmResponse(response.content, slideTypes, opts, blueprint);
|
|
812
|
+
if (parsed) return parsed;
|
|
813
|
+
} catch {
|
|
814
|
+
}
|
|
815
|
+
return buildFallback(blueprint, opts, slideTypes);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/generators/generateComplianceDoc.ts
|
|
819
|
+
var FRAMEWORK_SPECS = {
|
|
820
|
+
SOX: {
|
|
821
|
+
fullName: "Sarbanes-Oxley Act (SOX)",
|
|
822
|
+
focusAreas: [
|
|
823
|
+
"Internal controls over financial reporting (ICFR)",
|
|
824
|
+
"Access control and segregation of duties",
|
|
825
|
+
"Change management procedures",
|
|
826
|
+
"Audit trail completeness",
|
|
827
|
+
"IT general controls (ITGCs)"
|
|
828
|
+
],
|
|
829
|
+
requiredSections: [
|
|
830
|
+
"Scope and System Boundaries",
|
|
831
|
+
"IT General Controls",
|
|
832
|
+
"Access Control and Segregation of Duties",
|
|
833
|
+
"Change Management",
|
|
834
|
+
"Audit Trail and Logging",
|
|
835
|
+
"Financial Reporting Controls"
|
|
836
|
+
],
|
|
837
|
+
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."
|
|
838
|
+
},
|
|
839
|
+
ISO27001: {
|
|
840
|
+
fullName: "ISO/IEC 27001:2022 Information Security Management",
|
|
841
|
+
focusAreas: [
|
|
842
|
+
"Information security policies (Clause 5)",
|
|
843
|
+
"Risk assessment and treatment (Clause 6)",
|
|
844
|
+
"Information assets classification",
|
|
845
|
+
"Access control (Annex A 5.15\u20135.18)",
|
|
846
|
+
"Cryptography (Annex A 8.24)",
|
|
847
|
+
"Supplier relationships (Annex A 5.19\u20135.22)"
|
|
848
|
+
],
|
|
849
|
+
requiredSections: [
|
|
850
|
+
"Scope and Context",
|
|
851
|
+
"Information Assets",
|
|
852
|
+
"Risk Assessment",
|
|
853
|
+
"Access Control",
|
|
854
|
+
"Cryptography and Data Protection",
|
|
855
|
+
"Incident Management",
|
|
856
|
+
"Business Continuity"
|
|
857
|
+
],
|
|
858
|
+
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."
|
|
859
|
+
},
|
|
860
|
+
GDPR: {
|
|
861
|
+
fullName: "General Data Protection Regulation (GDPR)",
|
|
862
|
+
focusAreas: [
|
|
863
|
+
"Article 30 Records of Processing Activities (ROPA)",
|
|
864
|
+
"Lawful basis for processing",
|
|
865
|
+
"Data subject rights (Articles 15\u201322)",
|
|
866
|
+
"Data retention and deletion",
|
|
867
|
+
"Data transfers (Chapter V)",
|
|
868
|
+
"Privacy by design and default (Article 25)"
|
|
869
|
+
],
|
|
870
|
+
requiredSections: [
|
|
871
|
+
"Records of Processing Activities (ROPA)",
|
|
872
|
+
"Lawful Basis for Processing",
|
|
873
|
+
"Data Subject Rights",
|
|
874
|
+
"Data Retention Policy",
|
|
875
|
+
"International Data Transfers",
|
|
876
|
+
"Privacy by Design",
|
|
877
|
+
"Data Breach Response"
|
|
878
|
+
],
|
|
879
|
+
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."
|
|
880
|
+
},
|
|
881
|
+
SOC2: {
|
|
882
|
+
fullName: "SOC 2 Type II (Trust Services Criteria)",
|
|
883
|
+
focusAreas: [
|
|
884
|
+
"Security (Common Criteria \u2014 CC)",
|
|
885
|
+
"Availability (Availability Criteria \u2014 A)",
|
|
886
|
+
"Confidentiality (Confidentiality Criteria \u2014 C)",
|
|
887
|
+
"Processing Integrity (PI)",
|
|
888
|
+
"Privacy (P)"
|
|
889
|
+
],
|
|
890
|
+
requiredSections: [
|
|
891
|
+
"System Description",
|
|
892
|
+
"Security \u2014 Common Criteria",
|
|
893
|
+
"Availability",
|
|
894
|
+
"Confidentiality",
|
|
895
|
+
"Change Management",
|
|
896
|
+
"Monitoring",
|
|
897
|
+
"Vendor Management"
|
|
898
|
+
],
|
|
899
|
+
sectionGuidance: "Follow the AICPA Trust Services Criteria. For each section, identify relevant capabilities, current controls, and any gaps versus the criteria."
|
|
900
|
+
},
|
|
901
|
+
CUSTOM: {
|
|
902
|
+
fullName: "Custom Compliance Framework",
|
|
903
|
+
focusAreas: [
|
|
904
|
+
"Scope and applicability",
|
|
905
|
+
"Control requirements",
|
|
906
|
+
"Risk assessment",
|
|
907
|
+
"Monitoring and audit"
|
|
908
|
+
],
|
|
909
|
+
requiredSections: [
|
|
910
|
+
"Scope",
|
|
911
|
+
"Controls Inventory",
|
|
912
|
+
"Risk Assessment",
|
|
913
|
+
"Gaps and Recommendations"
|
|
914
|
+
],
|
|
915
|
+
sectionGuidance: "Generate a structured compliance document based on the codebase capabilities and general best practices."
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
function buildCodebaseContext(blueprint, opts) {
|
|
919
|
+
const lines = [
|
|
920
|
+
`## Codebase Overview`,
|
|
921
|
+
`Project: ${opts.projectName}`,
|
|
922
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
923
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`
|
|
924
|
+
];
|
|
925
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
|
|
926
|
+
lines.push(``, `## Most-imported files (likely critical components)`);
|
|
927
|
+
for (const f of topFiles) {
|
|
928
|
+
lines.push(`- ${f.path} [${f.category ?? "unknown"}] \u2014 imported by ${f.importedByCount ?? 0} modules`);
|
|
929
|
+
}
|
|
930
|
+
return lines.join("\n");
|
|
931
|
+
}
|
|
932
|
+
function buildAmberContext(amber) {
|
|
933
|
+
const lines = [`## AMBER Capability Registry`, amber.summary, ``];
|
|
934
|
+
for (const cap of amber.capabilities.slice(0, 20)) {
|
|
935
|
+
const tags = [cap.criticality, cap.lifecycle];
|
|
936
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
937
|
+
if (cap.hasTests) tags.push("documented");
|
|
938
|
+
lines.push(
|
|
939
|
+
`- ${cap.name} (${cap.id}) [${tags.join(", ")}]` + (cap.description ? `: ${cap.description}` : "")
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return lines.join("\n");
|
|
943
|
+
}
|
|
944
|
+
function buildComplianceContext(ctx, framework) {
|
|
945
|
+
const lines = [
|
|
946
|
+
`## PRISM Compliance Analysis`,
|
|
947
|
+
`Framework: ${ctx.framework}`,
|
|
948
|
+
`Total capabilities: ${ctx.totalCapabilities}`,
|
|
949
|
+
`Risk summary \u2014 High: ${ctx.riskSummary.high}, Medium: ${ctx.riskSummary.medium}, Low: ${ctx.riskSummary.low}`,
|
|
950
|
+
``,
|
|
951
|
+
`### Critical / High Capabilities`
|
|
952
|
+
];
|
|
953
|
+
for (const cap of ctx.criticalCapabilities.slice(0, 15)) {
|
|
954
|
+
const tags = [cap.criticality];
|
|
955
|
+
if (cap.accessControl) tags.push("access-control");
|
|
956
|
+
if (cap.auditTrail) tags.push("audit-trail");
|
|
957
|
+
if (cap.dataProcessing) tags.push("data-processing");
|
|
958
|
+
if (!cap.testCoverage) tags.push("no-doc");
|
|
959
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
960
|
+
lines.push(`- ${cap.name} (${cap.id}) [${tags.join(", ")}]`);
|
|
961
|
+
}
|
|
962
|
+
if (framework === "SOX" && ctx.sox) {
|
|
963
|
+
lines.push(``, `### SOX: Financial capabilities: ${ctx.sox.financialCapabilities.join(", ") || "none identified"}`);
|
|
964
|
+
lines.push(`SOX: Access-controlled: ${ctx.sox.accessControlled.join(", ") || "none identified"}`);
|
|
965
|
+
}
|
|
966
|
+
if (framework === "GDPR" && ctx.gdpr) {
|
|
967
|
+
lines.push(``, `### GDPR: Data processing capabilities: ${ctx.gdpr.dataProcessingCapabilities.join(", ") || "none identified"}`);
|
|
968
|
+
lines.push(`GDPR: Retention capabilities: ${ctx.gdpr.retentionCapabilities.join(", ") || "none identified"}`);
|
|
969
|
+
}
|
|
970
|
+
if (framework === "ISO27001" && ctx.iso27001) {
|
|
971
|
+
lines.push(``, `### ISO27001: Information assets: ${ctx.iso27001.informationAssets.join(", ") || "none identified"}`);
|
|
972
|
+
lines.push(`ISO27001: Security controls: ${ctx.iso27001.securityControls.join(", ") || "none identified"}`);
|
|
973
|
+
}
|
|
974
|
+
if (framework === "SOC2" && ctx.soc2) {
|
|
975
|
+
lines.push(``, `### SOC2: Security capabilities: ${ctx.soc2.securityCapabilities.join(", ") || "none identified"}`);
|
|
976
|
+
lines.push(`SOC2: Availability capabilities: ${ctx.soc2.availabilityCapabilities.join(", ") || "none identified"}`);
|
|
977
|
+
}
|
|
978
|
+
return lines.join("\n");
|
|
979
|
+
}
|
|
980
|
+
function buildSystemPrompt8(opts) {
|
|
981
|
+
const toneMap = {
|
|
982
|
+
formal: "You are a senior compliance officer writing a formal regulatory compliance document for external auditors and regulators.",
|
|
983
|
+
technical: "You are a security-focused software architect writing a technical compliance assessment grounded in code-level evidence.",
|
|
984
|
+
executive: "You are a Chief Compliance Officer writing a concise executive summary compliance report for board-level review."
|
|
985
|
+
};
|
|
986
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
987
|
+
}
|
|
988
|
+
function buildUserPrompt8(opts) {
|
|
989
|
+
const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
|
|
990
|
+
const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
|
|
991
|
+
const contextParts = [
|
|
992
|
+
buildCodebaseContext(opts.blueprint, opts)
|
|
993
|
+
];
|
|
994
|
+
if (opts.amberContext) contextParts.push(buildAmberContext(opts.amberContext));
|
|
995
|
+
if (opts.complianceContext) contextParts.push(buildComplianceContext(opts.complianceContext, opts.framework));
|
|
996
|
+
const org = opts.organizationName ? `Organization: ${opts.organizationName}` : "";
|
|
997
|
+
const recsInstruction = opts.includeRecommendations ? 'Include a "recommendations" array with 3\u20137 actionable remediation items.' : 'Set "recommendations" to an empty array.';
|
|
998
|
+
return [
|
|
999
|
+
`Generate a ${frameworkName} compliance document for: ${opts.projectName}`,
|
|
1000
|
+
org,
|
|
1001
|
+
`Tone: ${opts.tone}`,
|
|
1002
|
+
``,
|
|
1003
|
+
contextParts.join("\n\n"),
|
|
1004
|
+
``,
|
|
1005
|
+
`## Framework: ${frameworkName}`,
|
|
1006
|
+
`Focus areas:`,
|
|
1007
|
+
spec.focusAreas.map((f) => `- ${f}`).join("\n"),
|
|
1008
|
+
``,
|
|
1009
|
+
spec.sectionGuidance,
|
|
1010
|
+
``,
|
|
1011
|
+
`Required sections: ${spec.requiredSections.join(", ")}`,
|
|
1012
|
+
``,
|
|
1013
|
+
`## Output Format`,
|
|
1014
|
+
`Return a single JSON object with this exact shape:`,
|
|
1015
|
+
`{`,
|
|
1016
|
+
` "documentTitle": "<formal document title>",`,
|
|
1017
|
+
` "executiveSummary": "<3\u20135 sentences summarising compliance posture>",`,
|
|
1018
|
+
` "confidentiality": "INTERNAL" | "CONFIDENTIAL" | "PUBLIC",`,
|
|
1019
|
+
` "complianceGaps": ["<gap 1>", "<gap 2>"],`,
|
|
1020
|
+
` "recommendations": ["<rec 1>", "<rec 2>"],`,
|
|
1021
|
+
` "sections": [`,
|
|
1022
|
+
` {`,
|
|
1023
|
+
` "title": "<section title>",`,
|
|
1024
|
+
` "content": "<substantive section prose, 2\u20134 paragraphs>",`,
|
|
1025
|
+
` "capabilities": ["<capability id 1>"],`,
|
|
1026
|
+
` "riskLevel": "high" | "medium" | "low",`,
|
|
1027
|
+
` "status": "compliant" | "partial" | "gap"`,
|
|
1028
|
+
` }`,
|
|
1029
|
+
` ]`,
|
|
1030
|
+
`}`,
|
|
1031
|
+
``,
|
|
1032
|
+
`Rules:`,
|
|
1033
|
+
`- Derive ALL content from the codebase context above \u2014 no invented facts.`,
|
|
1034
|
+
`- Each section MUST reference real capability IDs from the AMBER registry where available.`,
|
|
1035
|
+
`- complianceGaps: list specific gaps found (missing controls, undocumented capabilities, drift).`,
|
|
1036
|
+
recsInstruction,
|
|
1037
|
+
`- confidentiality: CONFIDENTIAL for SOX/ISO27001/SOC2, INTERNAL for GDPR/CUSTOM.`,
|
|
1038
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1039
|
+
].filter(Boolean).join("\n");
|
|
1040
|
+
}
|
|
1041
|
+
function buildFallback2(opts) {
|
|
1042
|
+
const spec = FRAMEWORK_SPECS[opts.framework] ?? FRAMEWORK_SPECS["CUSTOM"];
|
|
1043
|
+
const frameworkName = opts.framework === "CUSTOM" ? opts.customFramework ?? "Custom Framework" : spec.fullName;
|
|
1044
|
+
const totalCaps = opts.complianceContext?.totalCapabilities ?? opts.amberContext?.capabilities.length ?? 0;
|
|
1045
|
+
const highRisk = opts.complianceContext?.riskSummary.high ?? 0;
|
|
1046
|
+
const sections = spec.requiredSections.map((title) => ({
|
|
1047
|
+
title,
|
|
1048
|
+
content: `Assessment of ${title.toLowerCase()} for ${opts.projectName}. Based on Blueprint analysis, ${totalCaps} capabilities were identified for review under ${frameworkName}.`,
|
|
1049
|
+
capabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
|
|
1050
|
+
riskLevel: "medium",
|
|
1051
|
+
status: "partial"
|
|
1052
|
+
}));
|
|
1053
|
+
return {
|
|
1054
|
+
framework: opts.framework,
|
|
1055
|
+
documentTitle: `${opts.projectName} \u2014 ${frameworkName} Compliance Assessment`,
|
|
1056
|
+
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.`,
|
|
1057
|
+
sections,
|
|
1058
|
+
complianceGaps: [
|
|
1059
|
+
`${totalCaps - (opts.amberContext?.taggedFiles ?? 0)} files lack capability annotations \u2014 compliance traceability is incomplete.`,
|
|
1060
|
+
`${opts.amberContext?.driftedCapabilities ?? 0} capabilities have documentation drift \u2014 controls may be misrepresented.`
|
|
1061
|
+
],
|
|
1062
|
+
recommendations: opts.includeRecommendations ? [
|
|
1063
|
+
"Ensure all critical capabilities have up-to-date @amber-doc annotations.",
|
|
1064
|
+
"Implement automated compliance scanning in CI/CD pipeline.",
|
|
1065
|
+
"Conduct quarterly capability review against framework requirements."
|
|
1066
|
+
] : [],
|
|
1067
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1068
|
+
confidentiality: opts.framework === "GDPR" || opts.framework === "CUSTOM" ? "INTERNAL" : "CONFIDENTIAL"
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function parseLlmResponse2(raw, opts) {
|
|
1072
|
+
let text = raw.trim();
|
|
1073
|
+
if (text.startsWith("```")) {
|
|
1074
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1075
|
+
}
|
|
1076
|
+
let parsed;
|
|
1077
|
+
try {
|
|
1078
|
+
parsed = JSON.parse(text);
|
|
1079
|
+
} catch {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
1083
|
+
if (rawSections.length === 0) return null;
|
|
1084
|
+
const sections = rawSections.map((s) => {
|
|
1085
|
+
const sec = s ?? {};
|
|
1086
|
+
return {
|
|
1087
|
+
title: typeof sec.title === "string" ? sec.title : "Section",
|
|
1088
|
+
content: typeof sec.content === "string" ? sec.content : "",
|
|
1089
|
+
capabilities: Array.isArray(sec.capabilities) ? sec.capabilities.filter((c) => typeof c === "string") : [],
|
|
1090
|
+
...typeof sec.riskLevel === "string" && ["high", "medium", "low"].includes(sec.riskLevel) ? { riskLevel: sec.riskLevel } : {},
|
|
1091
|
+
...typeof sec.status === "string" && ["compliant", "partial", "gap"].includes(sec.status) ? { status: sec.status } : {}
|
|
1092
|
+
};
|
|
1093
|
+
});
|
|
1094
|
+
const confidentiality = parsed.confidentiality === "CONFIDENTIAL" ? "CONFIDENTIAL" : parsed.confidentiality === "PUBLIC" ? "PUBLIC" : "INTERNAL";
|
|
1095
|
+
return {
|
|
1096
|
+
framework: opts.framework,
|
|
1097
|
+
documentTitle: typeof parsed.documentTitle === "string" ? parsed.documentTitle : `${opts.projectName} \u2014 Compliance Assessment`,
|
|
1098
|
+
sections,
|
|
1099
|
+
executiveSummary: typeof parsed.executiveSummary === "string" ? parsed.executiveSummary : "",
|
|
1100
|
+
complianceGaps: Array.isArray(parsed.complianceGaps) ? parsed.complianceGaps.filter((g) => typeof g === "string") : [],
|
|
1101
|
+
recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations.filter((r) => typeof r === "string") : [],
|
|
1102
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1103
|
+
confidentiality
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
async function generateComplianceDoc(opts) {
|
|
1107
|
+
const systemPrompt = buildSystemPrompt8(opts);
|
|
1108
|
+
const userPrompt = buildUserPrompt8(opts);
|
|
1109
|
+
try {
|
|
1110
|
+
const response = await opts.llm.complete({
|
|
1111
|
+
systemPrompt,
|
|
1112
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
1113
|
+
maxTokens: 6e3
|
|
1114
|
+
});
|
|
1115
|
+
const parsed = parseLlmResponse2(response.content, opts);
|
|
1116
|
+
if (parsed) return parsed;
|
|
1117
|
+
} catch {
|
|
1118
|
+
}
|
|
1119
|
+
return buildFallback2(opts);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/generators/generateADR.ts
|
|
1123
|
+
function buildCodebaseContext2(blueprint, projectName) {
|
|
1124
|
+
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 8);
|
|
1125
|
+
const totalLoc = blueprint.files.reduce((sum, f) => sum + (f.lineCount ?? 0), 0);
|
|
1126
|
+
const lines = [
|
|
1127
|
+
`## Codebase Overview`,
|
|
1128
|
+
`Project: ${projectName}`,
|
|
1129
|
+
`Total files: ${blueprint.stats.totalFiles}`,
|
|
1130
|
+
`Total LOC: ${totalLoc.toLocaleString()}`,
|
|
1131
|
+
`Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
1132
|
+
``,
|
|
1133
|
+
`## Most-imported files (architectural hotspots)`
|
|
1134
|
+
];
|
|
1135
|
+
for (const f of topFiles) {
|
|
1136
|
+
lines.push(`- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} modules, ${f.lineCount ?? 0} lines`);
|
|
1137
|
+
}
|
|
1138
|
+
if (blueprint.edges.length > 0) {
|
|
1139
|
+
const edgeSample = blueprint.edges.slice(0, 8);
|
|
1140
|
+
lines.push(``, `## Dependency sample`);
|
|
1141
|
+
for (const e of edgeSample) {
|
|
1142
|
+
lines.push(`- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return lines.join("\n");
|
|
1146
|
+
}
|
|
1147
|
+
function buildAdrContext(ctx, focus, selectedCycle, selectedCapability) {
|
|
1148
|
+
const lines = [`## PRISM Analysis`];
|
|
1149
|
+
if (ctx.coherenceScore !== null) {
|
|
1150
|
+
lines.push(`Architecture Coherence Score: ${ctx.coherenceScore}/100`);
|
|
1151
|
+
}
|
|
1152
|
+
if (ctx.architecturalPatterns.length > 0) {
|
|
1153
|
+
lines.push(`Detected patterns: ${ctx.architecturalPatterns.join(", ")}`);
|
|
1154
|
+
}
|
|
1155
|
+
if (ctx.topRisks.length > 0) {
|
|
1156
|
+
lines.push(``, `### Top Architectural Risks`);
|
|
1157
|
+
for (const r of ctx.topRisks.slice(0, 5)) {
|
|
1158
|
+
lines.push(`- ${r.name} (${r.capabilityId}): ${r.reason}`);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
if ((focus === "cycles" || focus === "overall") && ctx.importCycles.length > 0) {
|
|
1162
|
+
lines.push(``, `### Import Cycles (${ctx.importCycles.length} detected)`);
|
|
1163
|
+
const cyclesToShow = selectedCycle ? ctx.importCycles.filter((c) => c.files.some((f) => selectedCycle.includes(f))) : ctx.importCycles.slice(0, 5);
|
|
1164
|
+
for (const c of cyclesToShow) {
|
|
1165
|
+
lines.push(`- [${c.severity}] ${c.files.join(" \u2192 ")}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if ((focus === "capabilities" || focus === "overall") && ctx.capabilityBoundaries.length > 0) {
|
|
1169
|
+
lines.push(``, `### Capability Boundaries (dependency edges)`);
|
|
1170
|
+
const boundaries = selectedCapability ? ctx.capabilityBoundaries.filter((b) => b.from === selectedCapability || b.to === selectedCapability) : ctx.capabilityBoundaries.slice(0, 10);
|
|
1171
|
+
for (const b of boundaries) {
|
|
1172
|
+
lines.push(`- ${b.from} \u2192 ${b.to} (${b.edgeCount} import edges)`);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if ((focus === "dependencies" || focus === "overall") && ctx.highChurnFiles.length > 0) {
|
|
1176
|
+
lines.push(``, `### High-Churn Files`);
|
|
1177
|
+
for (const f of ctx.highChurnFiles.slice(0, 8)) {
|
|
1178
|
+
const caps = f.capabilities.length > 0 ? ` [${f.capabilities.join(", ")}]` : "";
|
|
1179
|
+
lines.push(`- ${f.path}${caps} \u2014 ${f.commits} commits`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
if (ctx.orphanedFiles.length > 0) {
|
|
1183
|
+
lines.push(``, `### Orphaned Files (${ctx.orphanedFiles.length} untagged)`);
|
|
1184
|
+
for (const f of ctx.orphanedFiles.slice(0, 5)) {
|
|
1185
|
+
lines.push(`- ${f}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return lines.join("\n");
|
|
1189
|
+
}
|
|
1190
|
+
function buildAmberContext2(amber) {
|
|
1191
|
+
const lines = [`## AMBER Capability Registry`, amber.summary, ``];
|
|
1192
|
+
for (const cap of amber.capabilities.slice(0, 15)) {
|
|
1193
|
+
const tags = [cap.criticality, cap.lifecycle];
|
|
1194
|
+
if (cap.driftCount > 0) tags.push(`drift:${cap.driftCount}`);
|
|
1195
|
+
lines.push(
|
|
1196
|
+
`- ${cap.name} (${cap.id}) [${tags.join(", ")}]${cap.files.length > 0 ? ` \u2014 ${cap.files.length} files` : ""}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
return lines.join("\n");
|
|
1200
|
+
}
|
|
1201
|
+
function buildSystemPrompt9() {
|
|
1202
|
+
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.";
|
|
1203
|
+
}
|
|
1204
|
+
function buildUserPrompt9(opts) {
|
|
1205
|
+
const focusDescriptions = {
|
|
1206
|
+
overall: "Generate a comprehensive set of ADRs covering the most significant architectural decisions evident from import cycles, capability boundaries, dependency patterns, and code structure.",
|
|
1207
|
+
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.",
|
|
1208
|
+
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.",
|
|
1209
|
+
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."
|
|
1210
|
+
};
|
|
1211
|
+
const parts = [
|
|
1212
|
+
`Generate 3\u20137 Architecture Decision Records (ADRs) for: ${opts.projectName}`,
|
|
1213
|
+
`Focus: ${opts.focus} \u2014 ${focusDescriptions[opts.focus]}`,
|
|
1214
|
+
``,
|
|
1215
|
+
buildCodebaseContext2(opts.blueprint, opts.projectName)
|
|
1216
|
+
];
|
|
1217
|
+
if (opts.amberContext) {
|
|
1218
|
+
parts.push(``, buildAmberContext2(opts.amberContext));
|
|
1219
|
+
}
|
|
1220
|
+
if (opts.adrContext) {
|
|
1221
|
+
parts.push(``, buildAdrContext(opts.adrContext, opts.focus, opts.selectedCycle, opts.selectedCapability));
|
|
1222
|
+
}
|
|
1223
|
+
if (opts.selectedCycle && opts.selectedCycle.length > 0) {
|
|
1224
|
+
parts.push(``, `## Selected Cycle for Focus`, opts.selectedCycle.join(" \u2192 "));
|
|
1225
|
+
}
|
|
1226
|
+
if (opts.selectedCapability) {
|
|
1227
|
+
parts.push(``, `## Selected Capability for Focus: ${opts.selectedCapability}`);
|
|
1228
|
+
}
|
|
1229
|
+
parts.push(
|
|
1230
|
+
``,
|
|
1231
|
+
`## Output Format`,
|
|
1232
|
+
`Return a single JSON object:`,
|
|
1233
|
+
`{`,
|
|
1234
|
+
` "summary": "<2\u20133 sentence overall architectural assessment>",`,
|
|
1235
|
+
` "technicalDebt": "<1\u20132 sentence estimate of technical debt severity>",`,
|
|
1236
|
+
` "adrs": [`,
|
|
1237
|
+
` {`,
|
|
1238
|
+
` "id": "ADR-001",`,
|
|
1239
|
+
` "title": "<short imperative title>",`,
|
|
1240
|
+
` "date": "${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}",`,
|
|
1241
|
+
` "status": "Proposed",`,
|
|
1242
|
+
` "context": "<what is the situation and why does this decision need to be made? 2\u20134 sentences>",`,
|
|
1243
|
+
` "decision": "<what was decided and why? 2\u20134 sentences with rationale>",`,
|
|
1244
|
+
` "consequences": {`,
|
|
1245
|
+
` "positive": ["<benefit 1>", "<benefit 2>"],`,
|
|
1246
|
+
` "negative": ["<drawback 1>"],`,
|
|
1247
|
+
` "neutral": ["<neutral fact>"]`,
|
|
1248
|
+
` },`,
|
|
1249
|
+
` "alternatives": ["<alternative option 1>", "<alternative option 2>"],`,
|
|
1250
|
+
` "relatedCapabilities": ["<capability-id-1>"],`,
|
|
1251
|
+
` "relatedFiles": ["<file/path.ts>"]`,
|
|
1252
|
+
` }`,
|
|
1253
|
+
` ]`,
|
|
1254
|
+
`}`,
|
|
1255
|
+
``,
|
|
1256
|
+
`Rules:`,
|
|
1257
|
+
`- IDs: ADR-001, ADR-002, \u2026 in order.`,
|
|
1258
|
+
`- Derive ALL content from the codebase data above \u2014 no invented facts.`,
|
|
1259
|
+
`- Each ADR MUST address a real pattern visible in the data (cycle, boundary, dependency).`,
|
|
1260
|
+
`- relatedCapabilities: reference real capability IDs from AMBER registry when available.`,
|
|
1261
|
+
`- relatedFiles: reference real file paths from the blueprint.`,
|
|
1262
|
+
`- status is always "Proposed" for new ADRs.`,
|
|
1263
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1264
|
+
);
|
|
1265
|
+
return parts.join("\n");
|
|
1266
|
+
}
|
|
1267
|
+
function buildFallback3(opts) {
|
|
1268
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1269
|
+
const adrs = [];
|
|
1270
|
+
if (opts.focus === "cycles" || opts.focus === "overall") {
|
|
1271
|
+
const cycleFiles = opts.adrContext?.importCycles[0]?.files ?? opts.selectedCycle ?? [];
|
|
1272
|
+
adrs.push({
|
|
1273
|
+
id: "ADR-001",
|
|
1274
|
+
title: "Introduce Module Boundary Abstractions to Break Import Cycles",
|
|
1275
|
+
date: today,
|
|
1276
|
+
status: "Proposed",
|
|
1277
|
+
context: `${opts.projectName} contains ${opts.adrContext?.importCycles.length ?? "multiple"} detected import cycles. Circular dependencies create tight coupling that makes independent testing and deployment difficult.`,
|
|
1278
|
+
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.",
|
|
1279
|
+
consequences: {
|
|
1280
|
+
positive: ["Enables independent testing of cyclic modules", "Reduces coupling between layers"],
|
|
1281
|
+
negative: ["Requires refactoring existing imports", "Short-term increase in file count"],
|
|
1282
|
+
neutral: ["Aligns with Dependency Inversion Principle"]
|
|
1283
|
+
},
|
|
1284
|
+
alternatives: ["Extract shared state to a separate module", "Use event-driven communication between modules"],
|
|
1285
|
+
relatedCapabilities: opts.amberContext?.capabilities.slice(0, 2).map((c) => c.id) ?? [],
|
|
1286
|
+
relatedFiles: cycleFiles.slice(0, 3)
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
if (opts.focus === "capabilities" || opts.focus === "overall") {
|
|
1290
|
+
adrs.push({
|
|
1291
|
+
id: `ADR-00${adrs.length + 1}`,
|
|
1292
|
+
title: "Enforce Explicit Capability Boundaries with Barrel Exports",
|
|
1293
|
+
date: today,
|
|
1294
|
+
status: "Proposed",
|
|
1295
|
+
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.`,
|
|
1296
|
+
decision: "Each capability should expose a single barrel index file. External modules import only from the barrel, never from internal implementation files.",
|
|
1297
|
+
consequences: {
|
|
1298
|
+
positive: ["Capability-level API surfaces become explicit", "Refactoring internals doesn't break consumers"],
|
|
1299
|
+
negative: ["Requires creating barrel files for each capability", "Barrel files need maintenance"],
|
|
1300
|
+
neutral: ["Standard pattern in large TypeScript codebases"]
|
|
1301
|
+
},
|
|
1302
|
+
alternatives: ["Use ESLint import boundaries rules", "Monorepo packages per capability"],
|
|
1303
|
+
relatedCapabilities: opts.amberContext?.capabilities.slice(0, 3).map((c) => c.id) ?? [],
|
|
1304
|
+
relatedFiles: []
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
adrs.push({
|
|
1308
|
+
id: `ADR-00${adrs.length + 1}`,
|
|
1309
|
+
title: "Establish Architectural Fitness Functions for Ongoing Compliance",
|
|
1310
|
+
date: today,
|
|
1311
|
+
status: "Proposed",
|
|
1312
|
+
context: `The ${opts.projectName} codebase currently lacks automated enforcement of architectural constraints. Without fitness functions, architectural drift accumulates silently.`,
|
|
1313
|
+
decision: "Integrate PRISM / prism0x2A scans into CI to track coherence score, import cycles, and capability drift. Gate merges on cycle count not increasing.",
|
|
1314
|
+
consequences: {
|
|
1315
|
+
positive: ["Architectural health is continuously measured", "Teams get early warning of degradation"],
|
|
1316
|
+
negative: ["CI time increases slightly", "Requires PRISM setup in CI environment"],
|
|
1317
|
+
neutral: ["Shift from reactive to proactive architecture governance"]
|
|
1318
|
+
},
|
|
1319
|
+
alternatives: ["Manual architecture review cadence", "Weekly automated reports only"],
|
|
1320
|
+
relatedCapabilities: [],
|
|
1321
|
+
relatedFiles: []
|
|
1322
|
+
});
|
|
1323
|
+
return {
|
|
1324
|
+
adrs,
|
|
1325
|
+
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.`,
|
|
1326
|
+
technicalDebt: "Moderate-to-high architectural debt evident from cycle patterns and implicit capability boundaries. Estimated 3\u20136 engineering weeks to implement the proposed decisions."
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function parseLlmResponse3(raw) {
|
|
1330
|
+
let text = raw.trim();
|
|
1331
|
+
if (text.startsWith("```")) {
|
|
1332
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1333
|
+
}
|
|
1334
|
+
let parsed;
|
|
1335
|
+
try {
|
|
1336
|
+
parsed = JSON.parse(text);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
const rawAdrs = Array.isArray(parsed.adrs) ? parsed.adrs : [];
|
|
1341
|
+
if (rawAdrs.length === 0) return null;
|
|
1342
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1343
|
+
const adrs = rawAdrs.map((a, idx) => {
|
|
1344
|
+
const adr = a ?? {};
|
|
1345
|
+
const consequences = typeof adr.consequences === "object" && adr.consequences !== null ? adr.consequences : {};
|
|
1346
|
+
const status = typeof adr.status === "string" && ["Proposed", "Accepted", "Deprecated", "Superseded"].includes(adr.status) ? adr.status : "Proposed";
|
|
1347
|
+
return {
|
|
1348
|
+
id: typeof adr.id === "string" ? adr.id : `ADR-${String(idx + 1).padStart(3, "0")}`,
|
|
1349
|
+
title: typeof adr.title === "string" ? adr.title : `Decision ${idx + 1}`,
|
|
1350
|
+
date: typeof adr.date === "string" ? adr.date : today,
|
|
1351
|
+
status,
|
|
1352
|
+
context: typeof adr.context === "string" ? adr.context : "",
|
|
1353
|
+
decision: typeof adr.decision === "string" ? adr.decision : "",
|
|
1354
|
+
consequences: {
|
|
1355
|
+
positive: Array.isArray(consequences.positive) ? consequences.positive.filter((s) => typeof s === "string") : [],
|
|
1356
|
+
negative: Array.isArray(consequences.negative) ? consequences.negative.filter((s) => typeof s === "string") : [],
|
|
1357
|
+
neutral: Array.isArray(consequences.neutral) ? consequences.neutral.filter((s) => typeof s === "string") : []
|
|
1358
|
+
},
|
|
1359
|
+
alternatives: Array.isArray(adr.alternatives) ? adr.alternatives.filter((s) => typeof s === "string") : [],
|
|
1360
|
+
relatedCapabilities: Array.isArray(adr.relatedCapabilities) ? adr.relatedCapabilities.filter((s) => typeof s === "string") : [],
|
|
1361
|
+
relatedFiles: Array.isArray(adr.relatedFiles) ? adr.relatedFiles.filter((s) => typeof s === "string") : []
|
|
1362
|
+
};
|
|
1363
|
+
});
|
|
1364
|
+
return {
|
|
1365
|
+
adrs,
|
|
1366
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
1367
|
+
technicalDebt: typeof parsed.technicalDebt === "string" ? parsed.technicalDebt : ""
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
async function generateADR(opts) {
|
|
1371
|
+
try {
|
|
1372
|
+
const response = await opts.llm.complete({
|
|
1373
|
+
systemPrompt: buildSystemPrompt9(),
|
|
1374
|
+
messages: [{ role: "user", content: buildUserPrompt9(opts) }],
|
|
1375
|
+
maxTokens: 6e3
|
|
1376
|
+
});
|
|
1377
|
+
const parsed = parseLlmResponse3(response.content);
|
|
1378
|
+
if (parsed) return parsed;
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
return buildFallback3(opts);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/generators/generateSprintRetro.ts
|
|
1385
|
+
function classifyCommit(subject) {
|
|
1386
|
+
const s = subject.toLowerCase();
|
|
1387
|
+
if (s.startsWith("feat") || s.includes("add ") || s.includes("implement") || s.includes("new ")) return "feature";
|
|
1388
|
+
if (s.startsWith("fix") || s.includes("bug") || s.includes("patch") || s.includes("hotfix")) return "fix";
|
|
1389
|
+
if (s.startsWith("refactor") || s.startsWith("refact") || s.includes("rewrite") || s.includes("restructure")) return "refactor";
|
|
1390
|
+
return "chore";
|
|
1391
|
+
}
|
|
1392
|
+
function buildContext2(opts) {
|
|
1393
|
+
const lines = [
|
|
1394
|
+
`## Sprint Context`,
|
|
1395
|
+
`Project: ${opts.projectName}`,
|
|
1396
|
+
`Sprint: ${opts.sprintName ?? "Current Sprint"}`,
|
|
1397
|
+
`Team size: ${opts.teamSize ?? "unknown"}`,
|
|
1398
|
+
`Tone: ${opts.tone}`,
|
|
1399
|
+
``
|
|
1400
|
+
];
|
|
1401
|
+
if (opts.gitContext) {
|
|
1402
|
+
const { commits, filesChanged, fromRef, toRef } = opts.gitContext;
|
|
1403
|
+
lines.push(
|
|
1404
|
+
`## Git Activity (${fromRef} \u2192 ${toRef})`,
|
|
1405
|
+
`Commits: ${commits.length}`,
|
|
1406
|
+
`Files changed: ${filesChanged}`,
|
|
1407
|
+
``,
|
|
1408
|
+
`### Commits`,
|
|
1409
|
+
...commits.slice(0, 30).map(
|
|
1410
|
+
(c) => `- [${classifyCommit(c.subject)}] ${c.subject}${c.author ? ` \u2014 ${c.author}` : ""}`
|
|
1411
|
+
),
|
|
1412
|
+
``
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
if (opts.scoreBefore !== void 0 || opts.scoreAfter !== void 0) {
|
|
1416
|
+
lines.push(
|
|
1417
|
+
`## Architecture Health`,
|
|
1418
|
+
`Score at sprint start: ${opts.scoreBefore ?? "unknown"}`,
|
|
1419
|
+
`Score at sprint end: ${opts.scoreAfter ?? "unknown"}`,
|
|
1420
|
+
``
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
if (opts.amberContext) {
|
|
1424
|
+
const { capabilities, driftedCapabilities, summary } = opts.amberContext;
|
|
1425
|
+
lines.push(
|
|
1426
|
+
`## AMBER Capability Layer`,
|
|
1427
|
+
summary,
|
|
1428
|
+
`Drifted capabilities: ${driftedCapabilities}`,
|
|
1429
|
+
``,
|
|
1430
|
+
`### Capabilities`,
|
|
1431
|
+
...capabilities.slice(0, 15).map(
|
|
1432
|
+
(c) => `- ${c.name} [${c.criticality}/${c.lifecycle}]${c.driftCount > 0 ? ` \u26A0 drift: ${c.driftCount} files` : ""}`
|
|
1433
|
+
),
|
|
1434
|
+
``
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
if (opts.greenContext) {
|
|
1438
|
+
lines.push(
|
|
1439
|
+
`## GREEN Cross-Layer Insights`,
|
|
1440
|
+
opts.greenContext.summary,
|
|
1441
|
+
``
|
|
1442
|
+
);
|
|
1443
|
+
if (opts.greenContext.topRisks.length > 0) {
|
|
1444
|
+
lines.push(
|
|
1445
|
+
`### Top Risks`,
|
|
1446
|
+
...opts.greenContext.topRisks.slice(0, 5).map(
|
|
1447
|
+
(r) => `- ${r.name}: ${r.reason} (impact: ${r.impact}/10)`
|
|
1448
|
+
),
|
|
1449
|
+
``
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return lines.join("\n");
|
|
1454
|
+
}
|
|
1455
|
+
function buildSystemPrompt10(opts) {
|
|
1456
|
+
const toneMap = {
|
|
1457
|
+
professional: "You are a senior engineering manager facilitating a data-driven sprint retrospective. Be precise and actionable.",
|
|
1458
|
+
casual: "You are a friendly scrum master running a fun and honest retrospective. Keep it light but insightful.",
|
|
1459
|
+
"team-friendly": "You are a team lead who wants everyone to feel heard. Balance honesty with encouragement."
|
|
1460
|
+
};
|
|
1461
|
+
return `${toneMap[opts.tone]} Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`;
|
|
1462
|
+
}
|
|
1463
|
+
function buildUserPrompt10(opts) {
|
|
1464
|
+
const context = buildContext2(opts);
|
|
1465
|
+
const sprintName = opts.sprintName ?? "Current Sprint";
|
|
1466
|
+
return [
|
|
1467
|
+
`Generate a sprint retrospective for: ${opts.projectName} \u2014 ${sprintName}`,
|
|
1468
|
+
``,
|
|
1469
|
+
context,
|
|
1470
|
+
``,
|
|
1471
|
+
`## Output Format`,
|
|
1472
|
+
`Return a single JSON object with this exact shape:`,
|
|
1473
|
+
`{`,
|
|
1474
|
+
` "sprintName": "${sprintName}",`,
|
|
1475
|
+
` "period": "<date range or sprint identifier>",`,
|
|
1476
|
+
` "summary": "<2-3 sentence executive summary of the sprint>",`,
|
|
1477
|
+
` "delivered": [`,
|
|
1478
|
+
` { "item": "<what was built>", "capability": "<AMBER capability id if applicable>", "type": "feature|fix|refactor|chore" }`,
|
|
1479
|
+
` ],`,
|
|
1480
|
+
` "healthDelta": {`,
|
|
1481
|
+
` "scoreBefore": <number or null>,`,
|
|
1482
|
+
` "scoreAfter": <number or null>,`,
|
|
1483
|
+
` "delta": <number or null>,`,
|
|
1484
|
+
` "verdict": "improved|stable|degraded",`,
|
|
1485
|
+
` "degradedCapabilities": ["<capability name>"]`,
|
|
1486
|
+
` },`,
|
|
1487
|
+
` "wentWell": ["<positive thing 1>", ...],`,
|
|
1488
|
+
` "improvements": ["<improvement needed 1>", ...],`,
|
|
1489
|
+
` "puzzles": ["<question or confusion 1>", ...],`,
|
|
1490
|
+
` "actions": ["<concrete action item 1>", ...],`,
|
|
1491
|
+
` "debtIncurred": ["<technical debt item 1>", ...],`,
|
|
1492
|
+
` "slideTitle": "<1 sentence sprint summary for a slide>",`,
|
|
1493
|
+
` "slidePoints": ["<key point 1>", "<key point 2>", "<key point 3>"]`,
|
|
1494
|
+
`}`,
|
|
1495
|
+
``,
|
|
1496
|
+
`Rules:`,
|
|
1497
|
+
`- delivered: derive from git commits, 5\u201315 items`,
|
|
1498
|
+
`- wentWell: 3\u20135 genuine positives from the data`,
|
|
1499
|
+
`- improvements: 3\u20135 concrete improvements (not vague)`,
|
|
1500
|
+
`- puzzles: 2\u20134 things that were unclear or confusing this sprint`,
|
|
1501
|
+
`- actions: 3\u20135 specific, assigned-sounding action items`,
|
|
1502
|
+
`- debtIncurred: 1\u20134 technical debt items visible from the commits/drift`,
|
|
1503
|
+
`- slidePoints: exactly 3 bullet strings \u2264 80 characters`,
|
|
1504
|
+
`- Derive everything from the context above \u2014 no invented facts`,
|
|
1505
|
+
`- Return ONLY valid JSON. No markdown. No prose outside the JSON.`
|
|
1506
|
+
].join("\n");
|
|
1507
|
+
}
|
|
1508
|
+
function computeHealthDelta(scoreBefore, scoreAfter, amberContext) {
|
|
1509
|
+
const before = scoreBefore ?? null;
|
|
1510
|
+
const after = scoreAfter ?? null;
|
|
1511
|
+
const delta = before !== null && after !== null ? after - before : null;
|
|
1512
|
+
let verdict = "stable";
|
|
1513
|
+
if (delta !== null) {
|
|
1514
|
+
if (delta > 2) verdict = "improved";
|
|
1515
|
+
else if (delta < -2) verdict = "degraded";
|
|
1516
|
+
}
|
|
1517
|
+
const degradedCapabilities = amberContext ? amberContext.capabilities.filter((c) => c.driftCount > 0 && (c.criticality === "critical" || c.criticality === "high")).map((c) => c.name) : [];
|
|
1518
|
+
return { scoreBefore: before, scoreAfter: after, delta, verdict, degradedCapabilities };
|
|
1519
|
+
}
|
|
1520
|
+
function buildFallback4(opts) {
|
|
1521
|
+
const sprintName = opts.sprintName ?? "Current Sprint";
|
|
1522
|
+
const commits = opts.gitContext?.commits ?? [];
|
|
1523
|
+
const healthDelta = computeHealthDelta(opts.scoreBefore, opts.scoreAfter, opts.amberContext);
|
|
1524
|
+
const delivered = commits.slice(0, 10).map((c) => ({
|
|
1525
|
+
item: c.subject,
|
|
1526
|
+
type: classifyCommit(c.subject)
|
|
1527
|
+
}));
|
|
1528
|
+
return {
|
|
1529
|
+
sprintName,
|
|
1530
|
+
period: opts.gitContext ? `${opts.gitContext.fromRef} \u2192 ${opts.gitContext.toRef}` : "This sprint",
|
|
1531
|
+
summary: `${opts.projectName} completed ${commits.length} commits this sprint across ${opts.gitContext?.filesChanged ?? 0} files.`,
|
|
1532
|
+
delivered,
|
|
1533
|
+
healthDelta,
|
|
1534
|
+
wentWell: [
|
|
1535
|
+
`${commits.filter((c) => classifyCommit(c.subject) === "feature").length} features shipped`,
|
|
1536
|
+
`${commits.filter((c) => classifyCommit(c.subject) === "fix").length} bugs fixed`,
|
|
1537
|
+
"Team maintained delivery cadence"
|
|
1538
|
+
],
|
|
1539
|
+
improvements: [
|
|
1540
|
+
"Reduce WIP by completing in-flight work before starting new",
|
|
1541
|
+
"Improve commit message clarity",
|
|
1542
|
+
"Address documentation drift in critical capabilities"
|
|
1543
|
+
],
|
|
1544
|
+
puzzles: [
|
|
1545
|
+
"Unclear ownership for cross-capability changes",
|
|
1546
|
+
"Test coverage strategy for new features"
|
|
1547
|
+
],
|
|
1548
|
+
actions: [
|
|
1549
|
+
"Schedule architecture review for flagged capabilities",
|
|
1550
|
+
"Set up automated coherence score tracking in CI",
|
|
1551
|
+
"Document top 3 architectural decisions made this sprint"
|
|
1552
|
+
],
|
|
1553
|
+
debtIncurred: healthDelta.degradedCapabilities.length > 0 ? healthDelta.degradedCapabilities.map((c) => `Documentation drift in ${c}`) : ["Review and address any undocumented capability changes"],
|
|
1554
|
+
slideTitle: `${sprintName}: ${commits.length} commits, ${opts.gitContext?.filesChanged ?? 0} files changed`,
|
|
1555
|
+
slidePoints: [
|
|
1556
|
+
`${commits.length} commits across ${opts.gitContext?.filesChanged ?? 0} files`,
|
|
1557
|
+
`Architecture health: ${healthDelta.verdict}`,
|
|
1558
|
+
`${healthDelta.degradedCapabilities.length} capabilities need attention`
|
|
1559
|
+
]
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
function parseLlmResponse4(raw, opts) {
|
|
1563
|
+
let text = raw.trim();
|
|
1564
|
+
if (text.startsWith("```")) {
|
|
1565
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1566
|
+
}
|
|
1567
|
+
let parsed;
|
|
1568
|
+
try {
|
|
1569
|
+
parsed = JSON.parse(text);
|
|
1570
|
+
} catch {
|
|
1571
|
+
return null;
|
|
1572
|
+
}
|
|
1573
|
+
const sprintName = typeof parsed.sprintName === "string" ? parsed.sprintName : opts.sprintName ?? "Current Sprint";
|
|
1574
|
+
const period = typeof parsed.period === "string" ? parsed.period : "";
|
|
1575
|
+
const summary = typeof parsed.summary === "string" ? parsed.summary : "";
|
|
1576
|
+
const rawDelivered = Array.isArray(parsed.delivered) ? parsed.delivered : [];
|
|
1577
|
+
const delivered = rawDelivered.filter((d) => typeof d.item === "string").map((d) => {
|
|
1578
|
+
const item = d;
|
|
1579
|
+
const type = ["feature", "fix", "refactor", "chore"].includes(item.type) ? item.type : "chore";
|
|
1580
|
+
return {
|
|
1581
|
+
item: item.item,
|
|
1582
|
+
...typeof item.capability === "string" ? { capability: item.capability } : {},
|
|
1583
|
+
type
|
|
1584
|
+
};
|
|
1585
|
+
});
|
|
1586
|
+
const hd = parsed.healthDelta ?? {};
|
|
1587
|
+
const scoreBeforeRaw = typeof hd.scoreBefore === "number" ? hd.scoreBefore : null;
|
|
1588
|
+
const scoreAfterRaw = typeof hd.scoreAfter === "number" ? hd.scoreAfter : null;
|
|
1589
|
+
const deltaRaw = typeof hd.delta === "number" ? hd.delta : null;
|
|
1590
|
+
const verdictRaw = ["improved", "stable", "degraded"].includes(hd.verdict) ? hd.verdict : "stable";
|
|
1591
|
+
const degradedCaps = Array.isArray(hd.degradedCapabilities) ? hd.degradedCapabilities.filter((x) => typeof x === "string") : [];
|
|
1592
|
+
const toStringArray = (key) => {
|
|
1593
|
+
const val = parsed[key];
|
|
1594
|
+
if (!Array.isArray(val)) return [];
|
|
1595
|
+
return val.filter((x) => typeof x === "string");
|
|
1596
|
+
};
|
|
1597
|
+
if (!summary && delivered.length === 0) return null;
|
|
1598
|
+
return {
|
|
1599
|
+
sprintName,
|
|
1600
|
+
period,
|
|
1601
|
+
summary,
|
|
1602
|
+
delivered,
|
|
1603
|
+
healthDelta: {
|
|
1604
|
+
scoreBefore: scoreBeforeRaw,
|
|
1605
|
+
scoreAfter: scoreAfterRaw,
|
|
1606
|
+
delta: deltaRaw,
|
|
1607
|
+
verdict: verdictRaw,
|
|
1608
|
+
degradedCapabilities: degradedCaps
|
|
1609
|
+
},
|
|
1610
|
+
wentWell: toStringArray("wentWell"),
|
|
1611
|
+
improvements: toStringArray("improvements"),
|
|
1612
|
+
puzzles: toStringArray("puzzles"),
|
|
1613
|
+
actions: toStringArray("actions"),
|
|
1614
|
+
debtIncurred: toStringArray("debtIncurred"),
|
|
1615
|
+
slideTitle: typeof parsed.slideTitle === "string" ? parsed.slideTitle : sprintName,
|
|
1616
|
+
slidePoints: toStringArray("slidePoints").slice(0, 3)
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
async function generateSprintRetro(opts) {
|
|
1620
|
+
try {
|
|
1621
|
+
const response = await opts.llm.complete({
|
|
1622
|
+
systemPrompt: buildSystemPrompt10(opts),
|
|
1623
|
+
messages: [{ role: "user", content: buildUserPrompt10(opts) }],
|
|
1624
|
+
maxTokens: 4096
|
|
1625
|
+
});
|
|
1626
|
+
const parsed = parseLlmResponse4(response.content, opts);
|
|
1627
|
+
if (parsed) return parsed;
|
|
1628
|
+
} catch {
|
|
1629
|
+
}
|
|
1630
|
+
return buildFallback4(opts);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// src/generators/generateNewsletter.ts
|
|
1634
|
+
function buildContext3(opts) {
|
|
1635
|
+
const { digestContext: d, amberContext, projectName, teamName, targetAudience, tone } = opts;
|
|
1636
|
+
const lines = [
|
|
1637
|
+
`## Newsletter Context`,
|
|
1638
|
+
`Project: ${projectName}`,
|
|
1639
|
+
`Team: ${teamName ?? "Engineering"}`,
|
|
1640
|
+
`Target audience: ${targetAudience}`,
|
|
1641
|
+
`Tone: ${tone}`,
|
|
1642
|
+
``,
|
|
1643
|
+
`## Weekly Digest \u2014 ${d.period}`,
|
|
1644
|
+
`Commits: ${d.commitCount}`,
|
|
1645
|
+
`Files changed: ${d.filesChanged}`,
|
|
1646
|
+
`Coherence grade: ${d.grade ?? "N/A"}`,
|
|
1647
|
+
`Score start: ${d.scoreStart ?? "N/A"}`,
|
|
1648
|
+
`Score end: ${d.scoreEnd ?? "N/A"}`,
|
|
1649
|
+
`Score delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"}`,
|
|
1650
|
+
`Health summary: ${d.healthSummary}`,
|
|
1651
|
+
``
|
|
1652
|
+
];
|
|
1653
|
+
if (d.topCommits.length > 0) {
|
|
1654
|
+
lines.push(`### Top commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
|
|
1655
|
+
}
|
|
1656
|
+
if (d.newDrifts.length > 0) {
|
|
1657
|
+
lines.push(`### New capability drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
|
|
1658
|
+
}
|
|
1659
|
+
if (d.resolvedDrifts.length > 0) {
|
|
1660
|
+
lines.push(`### Resolved drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
|
|
1661
|
+
}
|
|
1662
|
+
if (d.newRisks.length > 0) {
|
|
1663
|
+
lines.push(`### New risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
1664
|
+
}
|
|
1665
|
+
if (amberContext) {
|
|
1666
|
+
lines.push(`## AMBER Capability Summary`, amberContext.summary, ``);
|
|
1667
|
+
}
|
|
1668
|
+
return lines.join("\n");
|
|
1669
|
+
}
|
|
1670
|
+
function buildSystemPrompt11(opts) {
|
|
1671
|
+
const audienceMap = {
|
|
1672
|
+
"product-team": "You are writing a newsletter for the product engineering team \u2014 technical details are welcome.",
|
|
1673
|
+
management: "You are writing a newsletter for engineering managers \u2014 focus on velocity, quality, and risk.",
|
|
1674
|
+
stakeholders: "You are writing a newsletter for business stakeholders \u2014 translate technical work into business outcomes.",
|
|
1675
|
+
"all-hands": "You are writing a company-wide newsletter \u2014 accessible to everyone, inspiring, and concise."
|
|
1676
|
+
};
|
|
1677
|
+
const toneMap = {
|
|
1678
|
+
professional: "Use a professional, data-driven tone.",
|
|
1679
|
+
casual: "Use a friendly, conversational tone \u2014 contractions welcome.",
|
|
1680
|
+
accessible: "Use zero technical jargon. If you must mention a technical concept, define it in plain language immediately."
|
|
1681
|
+
};
|
|
1682
|
+
return [
|
|
1683
|
+
audienceMap[opts.targetAudience],
|
|
1684
|
+
toneMap[opts.tone],
|
|
1685
|
+
"Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object."
|
|
1686
|
+
].join(" ");
|
|
1687
|
+
}
|
|
1688
|
+
function buildUserPrompt11(opts) {
|
|
1689
|
+
const context = buildContext3(opts);
|
|
1690
|
+
const { projectName, teamName, targetAudience, tone, includeMetrics } = opts;
|
|
1691
|
+
return [
|
|
1692
|
+
`Generate a weekly engineering newsletter for: ${projectName}`,
|
|
1693
|
+
`Team: ${teamName ?? "Engineering"} | Audience: ${targetAudience} | Tone: ${tone}`,
|
|
1694
|
+
`Include metrics: ${includeMetrics}`,
|
|
1695
|
+
``,
|
|
1696
|
+
context,
|
|
1697
|
+
``,
|
|
1698
|
+
`## Output Format`,
|
|
1699
|
+
`Return a single JSON object with this exact shape:`,
|
|
1700
|
+
`{`,
|
|
1701
|
+
` "subject": "<email subject line, \u226460 chars>",`,
|
|
1702
|
+
` "preview": "<email preview text, \u226450 chars>",`,
|
|
1703
|
+
` "greeting": "<opening greeting>",`,
|
|
1704
|
+
` "headline": "<1-sentence hook that opens the newsletter>",`,
|
|
1705
|
+
` "sections": [`,
|
|
1706
|
+
` { "heading": "<section heading>", "body": "<2-4 sentences>", "type": "highlight|metrics|risk|shoutout|upcoming" }`,
|
|
1707
|
+
` ],`,
|
|
1708
|
+
` "metrics": [`,
|
|
1709
|
+
` { "label": "<metric name>", "value": "<value>", "trend": "up|down|stable" }`,
|
|
1710
|
+
` ],`,
|
|
1711
|
+
` "closing": "<1-2 sentence closing>",`,
|
|
1712
|
+
` "unsubscribeNote": "Reply STOP to unsubscribe from this digest.",`,
|
|
1713
|
+
` "slackVersion": "<Slack mrkdwn formatted version of the newsletter>",`,
|
|
1714
|
+
` "teamsVersion": "<Microsoft Teams Adaptive Card JSON as a string>",`,
|
|
1715
|
+
` "htmlVersion": "<complete HTML email with inline styles, dark-mode friendly>"`,
|
|
1716
|
+
`}`,
|
|
1717
|
+
``,
|
|
1718
|
+
`Rules:`,
|
|
1719
|
+
`- sections: 3\u20135 sections covering the highlights`,
|
|
1720
|
+
`- metrics: ${includeMetrics ? "include 3\u20135 key metrics from the digest" : "return empty array []"}`,
|
|
1721
|
+
`- slackVersion: use *bold*, _italic_, \`code\`, > blockquote, \u2022 bullets (mrkdwn)`,
|
|
1722
|
+
`- teamsVersion: valid Teams Adaptive Card JSON with TextBlock, FactSet elements`,
|
|
1723
|
+
`- htmlVersion: full HTML with <html><head><body>, inline CSS, no external resources`,
|
|
1724
|
+
`- subject: must reference the week/project and be compelling`,
|
|
1725
|
+
`- Derive all content from the digest context above \u2014 no invented facts`,
|
|
1726
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
1727
|
+
].join("\n");
|
|
1728
|
+
}
|
|
1729
|
+
function buildHtmlEmail(opts, sections, metrics) {
|
|
1730
|
+
const { projectName, teamName, digestContext: d } = opts;
|
|
1731
|
+
const sectionHtml = sections.map((s) => `
|
|
1732
|
+
<tr><td style="padding:20px 32px 0">
|
|
1733
|
+
<h2 style="margin:0 0 8px;font-size:16px;color:#e2e8f0">${s.heading}</h2>
|
|
1734
|
+
<p style="margin:0;color:#94a3b8;font-size:14px;line-height:1.6">${s.body}</p>
|
|
1735
|
+
</td></tr>`).join("");
|
|
1736
|
+
const metricsHtml = metrics.length > 0 ? `
|
|
1737
|
+
<tr><td style="padding:20px 32px 0">
|
|
1738
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0">
|
|
1739
|
+
<tr>${metrics.map((m) => `
|
|
1740
|
+
<td style="text-align:center;padding:12px;background:#1e293b;border-radius:8px;margin:4px">
|
|
1741
|
+
<div style="font-size:22px;font-weight:700;color:${m.trend === "up" ? "#34d399" : m.trend === "down" ? "#f87171" : "#94a3b8"}">${m.value}</div>
|
|
1742
|
+
<div style="font-size:12px;color:#64748b;margin-top:4px">${m.label}</div>
|
|
1743
|
+
</td>`).join("<td style='width:8px'></td>")}</tr>
|
|
1744
|
+
</table>
|
|
1745
|
+
</td></tr>` : "";
|
|
1746
|
+
return `<!DOCTYPE html>
|
|
1747
|
+
<html lang="en">
|
|
1748
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>${projectName} \u2014 Weekly Digest</title></head>
|
|
1749
|
+
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
|
1750
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
|
|
1751
|
+
<tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
|
|
1752
|
+
<h1 style="margin:0;font-size:24px;font-weight:700;color:#f1f5f9">${projectName}</h1>
|
|
1753
|
+
<p style="margin:4px 0 0;color:#64748b;font-size:14px">${teamName ?? "Engineering"} \xB7 ${d.period}</p>
|
|
1754
|
+
</td></tr>
|
|
1755
|
+
${sectionHtml}
|
|
1756
|
+
${metricsHtml}
|
|
1757
|
+
<tr><td style="padding:24px 32px;border-top:1px solid #1e293b;margin-top:24px">
|
|
1758
|
+
<p style="margin:0;color:#475569;font-size:12px">Generated by forge0x2B \xB7 Reply STOP to unsubscribe.</p>
|
|
1759
|
+
</td></tr>
|
|
1760
|
+
</table>
|
|
1761
|
+
</body>
|
|
1762
|
+
</html>`;
|
|
1763
|
+
}
|
|
1764
|
+
function buildSlackVersion(opts, sections, metrics) {
|
|
1765
|
+
const { projectName, digestContext: d } = opts;
|
|
1766
|
+
const lines = [
|
|
1767
|
+
`*${projectName} \u2014 Weekly Engineering Digest*`,
|
|
1768
|
+
`_${d.period}_`,
|
|
1769
|
+
``,
|
|
1770
|
+
`*Health:* ${d.healthSummary}`,
|
|
1771
|
+
``,
|
|
1772
|
+
...sections.map((s) => [`*${s.heading}*`, s.body, ""].join("\n"))
|
|
1773
|
+
];
|
|
1774
|
+
if (metrics.length > 0) {
|
|
1775
|
+
lines.push(
|
|
1776
|
+
`*Key Metrics*`,
|
|
1777
|
+
...metrics.map((m) => `\u2022 *${m.label}:* ${m.value} ${m.trend === "up" ? "\u2191" : m.trend === "down" ? "\u2193" : "\u2192"}`),
|
|
1778
|
+
``
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
lines.push(`_Generated by forge0x2B_`);
|
|
1782
|
+
return lines.join("\n");
|
|
1783
|
+
}
|
|
1784
|
+
function buildTeamsVersion(opts, sections) {
|
|
1785
|
+
const { projectName, digestContext: d } = opts;
|
|
1786
|
+
const card = {
|
|
1787
|
+
type: "AdaptiveCard",
|
|
1788
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
1789
|
+
version: "1.4",
|
|
1790
|
+
body: [
|
|
1791
|
+
{
|
|
1792
|
+
type: "TextBlock",
|
|
1793
|
+
text: `${projectName} \u2014 Weekly Engineering Digest`,
|
|
1794
|
+
weight: "Bolder",
|
|
1795
|
+
size: "Large",
|
|
1796
|
+
color: "Accent"
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
type: "TextBlock",
|
|
1800
|
+
text: d.period,
|
|
1801
|
+
isSubtle: true,
|
|
1802
|
+
spacing: "None"
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
type: "TextBlock",
|
|
1806
|
+
text: d.healthSummary,
|
|
1807
|
+
wrap: true,
|
|
1808
|
+
spacing: "Medium"
|
|
1809
|
+
},
|
|
1810
|
+
...sections.slice(0, 3).map((s) => ({
|
|
1811
|
+
type: "Container",
|
|
1812
|
+
spacing: "Medium",
|
|
1813
|
+
items: [
|
|
1814
|
+
{ type: "TextBlock", text: s.heading, weight: "Bolder", wrap: true },
|
|
1815
|
+
{ type: "TextBlock", text: s.body, wrap: true, isSubtle: true }
|
|
1816
|
+
]
|
|
1817
|
+
})),
|
|
1818
|
+
{
|
|
1819
|
+
type: "TextBlock",
|
|
1820
|
+
text: "Generated by forge0x2B",
|
|
1821
|
+
isSubtle: true,
|
|
1822
|
+
size: "Small",
|
|
1823
|
+
spacing: "Large"
|
|
1824
|
+
}
|
|
1825
|
+
]
|
|
1826
|
+
};
|
|
1827
|
+
return JSON.stringify(card);
|
|
1828
|
+
}
|
|
1829
|
+
function buildFallback5(opts) {
|
|
1830
|
+
const { projectName, teamName, digestContext: d, includeMetrics } = opts;
|
|
1831
|
+
const sections = [
|
|
1832
|
+
{
|
|
1833
|
+
heading: "This Week in Engineering",
|
|
1834
|
+
body: `The team shipped ${d.commitCount} commits across ${d.filesChanged} files during ${d.period}. ${d.healthSummary}`,
|
|
1835
|
+
type: "highlight"
|
|
1836
|
+
}
|
|
1837
|
+
];
|
|
1838
|
+
if (d.topCommits.length > 0) {
|
|
1839
|
+
sections.push({
|
|
1840
|
+
heading: "Highlights",
|
|
1841
|
+
body: d.topCommits.slice(0, 3).join(". ") + ".",
|
|
1842
|
+
type: "highlight"
|
|
1843
|
+
});
|
|
1844
|
+
}
|
|
1845
|
+
if (d.newRisks.length > 0) {
|
|
1846
|
+
sections.push({
|
|
1847
|
+
heading: "On Our Radar",
|
|
1848
|
+
body: `We identified ${d.newRisks.length} new risk${d.newRisks.length > 1 ? "s" : ""} this week: ${d.newRisks.slice(0, 2).join(", ")}.`,
|
|
1849
|
+
type: "risk"
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
const metrics = includeMetrics ? [
|
|
1853
|
+
{ label: "Commits", value: String(d.commitCount), trend: "stable" },
|
|
1854
|
+
{ label: "Files Changed", value: String(d.filesChanged), trend: "stable" },
|
|
1855
|
+
...d.grade ? [{ label: "Architecture Grade", value: d.grade, trend: d.scoreDelta !== null && d.scoreDelta > 0 ? "up" : d.scoreDelta !== null && d.scoreDelta < 0 ? "down" : "stable" }] : []
|
|
1856
|
+
] : [];
|
|
1857
|
+
return {
|
|
1858
|
+
subject: `${projectName} Engineering Digest \u2014 ${d.period}`,
|
|
1859
|
+
preview: `${d.commitCount} commits \xB7 ${d.healthSummary.slice(0, 40)}`,
|
|
1860
|
+
greeting: `Hello ${teamName ?? "team"},`,
|
|
1861
|
+
headline: `Here's what the engineering team shipped this week.`,
|
|
1862
|
+
sections,
|
|
1863
|
+
metrics,
|
|
1864
|
+
closing: `Thanks for reading. See you next week!`,
|
|
1865
|
+
unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
|
|
1866
|
+
slackVersion: buildSlackVersion(opts, sections, metrics),
|
|
1867
|
+
teamsVersion: buildTeamsVersion(opts, sections),
|
|
1868
|
+
htmlVersion: buildHtmlEmail(opts, sections, metrics)
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
function parseLlmResponse5(raw, opts) {
|
|
1872
|
+
let text = raw.trim();
|
|
1873
|
+
if (text.startsWith("```")) {
|
|
1874
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
1875
|
+
}
|
|
1876
|
+
let parsed;
|
|
1877
|
+
try {
|
|
1878
|
+
parsed = JSON.parse(text);
|
|
1879
|
+
} catch {
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
const getString = (key, fallback) => typeof parsed[key] === "string" ? parsed[key] : fallback;
|
|
1883
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
1884
|
+
const sections = rawSections.filter((s) => typeof s.heading === "string").map((s) => {
|
|
1885
|
+
const item = s;
|
|
1886
|
+
return {
|
|
1887
|
+
heading: item.heading,
|
|
1888
|
+
body: typeof item.body === "string" ? item.body : "",
|
|
1889
|
+
type: ["highlight", "metrics", "risk", "shoutout", "upcoming"].includes(item.type) ? item.type : "highlight"
|
|
1890
|
+
};
|
|
1891
|
+
});
|
|
1892
|
+
const rawMetrics = Array.isArray(parsed.metrics) ? parsed.metrics : [];
|
|
1893
|
+
const metrics = rawMetrics.filter((m) => typeof m.label === "string").map((m) => {
|
|
1894
|
+
const item = m;
|
|
1895
|
+
return {
|
|
1896
|
+
label: item.label,
|
|
1897
|
+
value: typeof item.value === "string" ? item.value : "",
|
|
1898
|
+
trend: ["up", "down", "stable"].includes(item.trend) ? item.trend : "stable"
|
|
1899
|
+
};
|
|
1900
|
+
});
|
|
1901
|
+
const subject = getString("subject", "");
|
|
1902
|
+
if (!subject) return null;
|
|
1903
|
+
const slackVersion = getString("slackVersion", "") || buildSlackVersion(opts, sections, metrics);
|
|
1904
|
+
const teamsVersion = getString("teamsVersion", "") || buildTeamsVersion(opts, sections);
|
|
1905
|
+
const htmlVersion = getString("htmlVersion", "") || buildHtmlEmail(opts, sections, metrics);
|
|
1906
|
+
return {
|
|
1907
|
+
subject,
|
|
1908
|
+
preview: getString("preview", "").slice(0, 50),
|
|
1909
|
+
greeting: getString("greeting", `Hello ${opts.teamName ?? "team"},`),
|
|
1910
|
+
headline: getString("headline", ""),
|
|
1911
|
+
sections,
|
|
1912
|
+
metrics,
|
|
1913
|
+
closing: getString("closing", ""),
|
|
1914
|
+
unsubscribeNote: "Reply STOP to unsubscribe from this digest.",
|
|
1915
|
+
slackVersion,
|
|
1916
|
+
teamsVersion,
|
|
1917
|
+
htmlVersion
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
async function generateNewsletter(opts) {
|
|
1921
|
+
try {
|
|
1922
|
+
const response = await opts.llm.complete({
|
|
1923
|
+
systemPrompt: buildSystemPrompt11(opts),
|
|
1924
|
+
messages: [{ role: "user", content: buildUserPrompt11(opts) }],
|
|
1925
|
+
maxTokens: 8192
|
|
1926
|
+
});
|
|
1927
|
+
const parsed = parseLlmResponse5(response.content, opts);
|
|
1928
|
+
if (parsed) return parsed;
|
|
1929
|
+
} catch {
|
|
1930
|
+
}
|
|
1931
|
+
return buildFallback5(opts);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// src/generators/generateRadio.ts
|
|
1935
|
+
function buildContext4(opts) {
|
|
1936
|
+
const { digestContext: d, amberContext, projectName, audience } = opts;
|
|
1937
|
+
const lines = [
|
|
1938
|
+
`## Architecture Radio Context`,
|
|
1939
|
+
`Project: ${projectName}`,
|
|
1940
|
+
`Audience: ${audience}`,
|
|
1941
|
+
`Period: ${d.period}`,
|
|
1942
|
+
``,
|
|
1943
|
+
`## Weekly Digest`,
|
|
1944
|
+
`Commits: ${d.commitCount}`,
|
|
1945
|
+
`Files changed: ${d.filesChanged}`,
|
|
1946
|
+
`Coherence grade: ${d.grade ?? "N/A"}`,
|
|
1947
|
+
`Score: ${d.scoreStart ?? "N/A"} \u2192 ${d.scoreEnd ?? "N/A"} (delta: ${d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A"})`,
|
|
1948
|
+
`Health: ${d.healthSummary}`,
|
|
1949
|
+
``
|
|
1950
|
+
];
|
|
1951
|
+
if (d.topCommits.length > 0) {
|
|
1952
|
+
lines.push(`### Top Commits`, ...d.topCommits.slice(0, 5).map((c) => `- ${c}`), ``);
|
|
1953
|
+
}
|
|
1954
|
+
if (d.newDrifts.length > 0) {
|
|
1955
|
+
lines.push(`### Capability Drifts`, ...d.newDrifts.map((c) => `- ${c}`), ``);
|
|
1956
|
+
}
|
|
1957
|
+
if (d.resolvedDrifts.length > 0) {
|
|
1958
|
+
lines.push(`### Resolved Drifts`, ...d.resolvedDrifts.map((c) => `- ${c}`), ``);
|
|
1959
|
+
}
|
|
1960
|
+
if (d.newRisks.length > 0) {
|
|
1961
|
+
lines.push(`### New Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
1962
|
+
}
|
|
1963
|
+
if (amberContext) {
|
|
1964
|
+
lines.push(
|
|
1965
|
+
`## AMBER Capability Summary`,
|
|
1966
|
+
`Total capabilities: ${amberContext.capabilities.length}`,
|
|
1967
|
+
`Drifted: ${amberContext.driftedCapabilities}`,
|
|
1968
|
+
`Tagged files: ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`,
|
|
1969
|
+
``,
|
|
1970
|
+
amberContext.summary,
|
|
1971
|
+
``
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
return lines.join("\n");
|
|
1975
|
+
}
|
|
1976
|
+
function buildSystemPrompt12(audience) {
|
|
1977
|
+
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.";
|
|
1978
|
+
return [
|
|
1979
|
+
`You are Architecture Radio \u2014 the daily briefing that turns raw engineering signal into clear, compelling communication.`,
|
|
1980
|
+
audienceInstructions,
|
|
1981
|
+
`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.`,
|
|
1982
|
+
`Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
|
|
1983
|
+
].join(" ");
|
|
1984
|
+
}
|
|
1985
|
+
function buildUserPrompt12(opts) {
|
|
1986
|
+
const context = buildContext4(opts);
|
|
1987
|
+
return [
|
|
1988
|
+
`Generate today's Architecture Radio briefing for: ${opts.projectName}`,
|
|
1989
|
+
`Audience: ${opts.audience}`,
|
|
1990
|
+
``,
|
|
1991
|
+
context,
|
|
1992
|
+
``,
|
|
1993
|
+
`## Output Format`,
|
|
1994
|
+
`Return a single JSON object with this exact shape:`,
|
|
1995
|
+
`{`,
|
|
1996
|
+
` "headline": "<1 powerful sentence capturing the most important thing \u2014 not a title, a statement>",`,
|
|
1997
|
+
` "slackVersion": "<~150 word Slack mrkdwn message \u2014 punchy, action-oriented, uses *bold*, _italic_, \u2022 bullets>",`,
|
|
1998
|
+
` "emailVersion": "<~400 word complete HTML email \u2014 full <html><head><body> with inline CSS, dark background, proper greeting, body paragraphs, clear CTA>",`,
|
|
1999
|
+
` "twitterVersion": "<insight worth sharing publicly \u2014 \u2264280 chars, no sensitive data, no project-specific names unless generic>"`,
|
|
2000
|
+
`}`,
|
|
2001
|
+
``,
|
|
2002
|
+
`Rules:`,
|
|
2003
|
+
`- headline: not a generic recap \u2014 capture what MATTERS most today`,
|
|
2004
|
+
`- slackVersion: start with the headline, use mrkdwn formatting`,
|
|
2005
|
+
`- emailVersion: full HTML with inline styles, dark-mode friendly (#0f172a background), greeting, 2-3 paragraphs, bold CTA at end`,
|
|
2006
|
+
`- twitterVersion: shareable engineering insight, safe for public, abstract enough for any team`,
|
|
2007
|
+
`- ${opts.audience === "executive" ? "No technical jargon anywhere \u2014 business language only" : "Be technically precise \u2014 engineers hate vague"}`,
|
|
2008
|
+
`- Derive all content from the digest context \u2014 no invented facts`,
|
|
2009
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
2010
|
+
].join("\n");
|
|
2011
|
+
}
|
|
2012
|
+
function buildFallback6(opts) {
|
|
2013
|
+
const { digestContext: d, projectName, audience } = opts;
|
|
2014
|
+
const isExec = audience === "executive";
|
|
2015
|
+
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}`;
|
|
2016
|
+
const slackVersion = [
|
|
2017
|
+
`*\u{1F4E1} Architecture Radio \u2014 ${d.period}*`,
|
|
2018
|
+
``,
|
|
2019
|
+
`*${headline}*`,
|
|
2020
|
+
``,
|
|
2021
|
+
`\u2022 *Health:* ${d.healthSummary}`,
|
|
2022
|
+
d.commitCount > 0 ? `\u2022 *Commits:* ${d.commitCount} across ${d.filesChanged} files` : "",
|
|
2023
|
+
d.newDrifts.length > 0 ? `\u2022 *Drifts:* ${d.newDrifts.slice(0, 3).join(", ")}` : "",
|
|
2024
|
+
d.newRisks.length > 0 ? `\u2022 *Risks:* ${d.newRisks.slice(0, 2).join(", ")}` : "",
|
|
2025
|
+
``,
|
|
2026
|
+
`_Generated by forge0x2B_`
|
|
2027
|
+
].filter(Boolean).join("\n");
|
|
2028
|
+
const emailVersion = `<!DOCTYPE html>
|
|
2029
|
+
<html lang="en">
|
|
2030
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Architecture Radio \u2014 ${projectName}</title></head>
|
|
2031
|
+
<body style="margin:0;padding:0;background:#0f172a;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif">
|
|
2032
|
+
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;margin:0 auto">
|
|
2033
|
+
<tr><td style="padding:32px 32px 16px;border-bottom:1px solid #1e293b">
|
|
2034
|
+
<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>
|
|
2035
|
+
<h1 style="margin:0;font-size:22px;font-weight:700;color:#f1f5f9;line-height:1.3">${headline}</h1>
|
|
2036
|
+
<p style="margin:8px 0 0;color:#64748b;font-size:13px">${d.period}</p>
|
|
2037
|
+
</td></tr>
|
|
2038
|
+
<tr><td style="padding:24px 32px">
|
|
2039
|
+
<p style="margin:0 0 16px;color:#94a3b8;font-size:14px;line-height:1.6">Hello,</p>
|
|
2040
|
+
<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>
|
|
2041
|
+
${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>` : ""}
|
|
2042
|
+
${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>` : ""}
|
|
2043
|
+
<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>
|
|
2044
|
+
</td></tr>
|
|
2045
|
+
</table>
|
|
2046
|
+
</body>
|
|
2047
|
+
</html>`;
|
|
2048
|
+
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}`;
|
|
2049
|
+
return { headline, slackVersion, emailVersion, twitterVersion };
|
|
2050
|
+
}
|
|
2051
|
+
function parseLlmResponse6(raw) {
|
|
2052
|
+
let text = raw.trim();
|
|
2053
|
+
if (text.startsWith("```")) {
|
|
2054
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
2055
|
+
}
|
|
2056
|
+
let parsed;
|
|
2057
|
+
try {
|
|
2058
|
+
parsed = JSON.parse(text);
|
|
2059
|
+
} catch {
|
|
2060
|
+
return null;
|
|
2061
|
+
}
|
|
2062
|
+
const getString = (key) => typeof parsed[key] === "string" ? parsed[key] : "";
|
|
2063
|
+
const headline = getString("headline");
|
|
2064
|
+
if (!headline) return null;
|
|
2065
|
+
return {
|
|
2066
|
+
headline,
|
|
2067
|
+
slackVersion: getString("slackVersion"),
|
|
2068
|
+
emailVersion: getString("emailVersion"),
|
|
2069
|
+
twitterVersion: getString("twitterVersion").slice(0, 280)
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
async function generateRadio(options) {
|
|
2073
|
+
try {
|
|
2074
|
+
const response = await options.llm.complete({
|
|
2075
|
+
systemPrompt: buildSystemPrompt12(options.audience),
|
|
2076
|
+
messages: [{ role: "user", content: buildUserPrompt12(options) }],
|
|
2077
|
+
maxTokens: 4096
|
|
2078
|
+
});
|
|
2079
|
+
const parsed = parseLlmResponse6(response.content);
|
|
2080
|
+
if (parsed) return parsed;
|
|
2081
|
+
} catch {
|
|
2082
|
+
}
|
|
2083
|
+
return buildFallback6(options);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// src/generators/generateArc42.ts
|
|
2087
|
+
var ARC42_ATTRIBUTION = `arc42 template \xA9 arc42.org, Creative Commons Attribution 4.0 International (CC-BY 4.0).
|
|
2088
|
+
See https://arc42.org for the original template and documentation.`;
|
|
2089
|
+
function buildSection5(opts) {
|
|
2090
|
+
const { amberContext, projectName } = opts;
|
|
2091
|
+
if (!amberContext || amberContext.capabilities.length === 0) {
|
|
2092
|
+
return `## 5. Building Block View
|
|
2093
|
+
|
|
2094
|
+
No AMBER capability data available. Run an AMBER scan to populate this section.
|
|
2095
|
+
|
|
2096
|
+
*This section would show the top-level decomposition of the system into building blocks (capabilities) and their relationships.*
|
|
2097
|
+
`;
|
|
2098
|
+
}
|
|
2099
|
+
const lines = [
|
|
2100
|
+
`## 5. Building Block View`,
|
|
2101
|
+
``,
|
|
2102
|
+
`*Source: PRISM AMBER capability registry*`,
|
|
2103
|
+
``,
|
|
2104
|
+
`### Level 1 \u2014 ${projectName} (Whitebox)`,
|
|
2105
|
+
``,
|
|
2106
|
+
`**Total capabilities:** ${amberContext.capabilities.length}`,
|
|
2107
|
+
`**Tagged files:** ${amberContext.taggedFiles} of ${amberContext.totalFiles} (${amberContext.taggedPercent.toFixed(0)}%)`,
|
|
2108
|
+
``,
|
|
2109
|
+
`| Capability | Criticality | Lifecycle | Files |`,
|
|
2110
|
+
`|------------|-------------|-----------|-------|`
|
|
2111
|
+
];
|
|
2112
|
+
for (const cap of amberContext.capabilities) {
|
|
2113
|
+
lines.push(
|
|
2114
|
+
`| ${cap.name} | ${cap.criticality} | ${cap.lifecycle} | ${cap.files.length} |`
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
if (amberContext.driftedCapabilities > 0) {
|
|
2118
|
+
lines.push(
|
|
2119
|
+
``,
|
|
2120
|
+
`> **Note:** ${amberContext.driftedCapabilities} capabilities have documentation drift (files modified since last doc update).`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
if (amberContext.orphanedFiles.length > 0) {
|
|
2124
|
+
lines.push(
|
|
2125
|
+
``,
|
|
2126
|
+
`### Orphaned Files (not assigned to any capability)`,
|
|
2127
|
+
``,
|
|
2128
|
+
...amberContext.orphanedFiles.slice(0, 10).map((f) => `- \`${f}\``),
|
|
2129
|
+
amberContext.orphanedFiles.length > 10 ? `- *... and ${amberContext.orphanedFiles.length - 10} more*` : ``
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
return lines.join("\n");
|
|
2133
|
+
}
|
|
2134
|
+
function buildSection9(opts) {
|
|
2135
|
+
const { digestContext: d } = opts;
|
|
2136
|
+
const lines = [
|
|
2137
|
+
`## 9. Architecture Decisions`,
|
|
2138
|
+
``,
|
|
2139
|
+
`*Source: PRISM architecture signals*`,
|
|
2140
|
+
``
|
|
2141
|
+
];
|
|
2142
|
+
if (d.newRisks.length > 0) {
|
|
2143
|
+
lines.push(
|
|
2144
|
+
`### Recent Architectural Risks Surfaced`,
|
|
2145
|
+
``,
|
|
2146
|
+
...d.newRisks.map((r) => `- ${r}`),
|
|
2147
|
+
``
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
if (d.newDrifts.length > 0 || d.resolvedDrifts.length > 0) {
|
|
2151
|
+
lines.push(
|
|
2152
|
+
`### Documentation State`,
|
|
2153
|
+
``
|
|
2154
|
+
);
|
|
2155
|
+
if (d.newDrifts.length > 0) {
|
|
2156
|
+
lines.push(
|
|
2157
|
+
`**New capability drifts** (${d.newDrifts.length}):`,
|
|
2158
|
+
...d.newDrifts.map((c) => `- ${c}`),
|
|
2159
|
+
``
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
if (d.resolvedDrifts.length > 0) {
|
|
2163
|
+
lines.push(
|
|
2164
|
+
`**Resolved drifts** (${d.resolvedDrifts.length}):`,
|
|
2165
|
+
...d.resolvedDrifts.map((c) => `- \u2713 ${c}`),
|
|
2166
|
+
``
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
lines.push(
|
|
2171
|
+
`### Placeholder`,
|
|
2172
|
+
``,
|
|
2173
|
+
`*Add ADRs (Architecture Decision Records) here. Use the forge0x2B ADR generator to produce MADR-format decisions from your codebase.*`,
|
|
2174
|
+
``,
|
|
2175
|
+
`| ID | Title | Status | Date |`,
|
|
2176
|
+
`|----|-------|--------|------|`,
|
|
2177
|
+
`| ADR-001 | *Example decision* | Proposed | ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)} |`
|
|
2178
|
+
);
|
|
2179
|
+
return lines.join("\n");
|
|
2180
|
+
}
|
|
2181
|
+
function buildSection10(opts) {
|
|
2182
|
+
const { digestContext: d } = opts;
|
|
2183
|
+
const lines = [
|
|
2184
|
+
`## 10. Quality Requirements`,
|
|
2185
|
+
``,
|
|
2186
|
+
`*Source: PRISM coherence metrics*`,
|
|
2187
|
+
``,
|
|
2188
|
+
`### Quality Tree`,
|
|
2189
|
+
``,
|
|
2190
|
+
`| Quality Goal | Scenario | Priority | Current State |`,
|
|
2191
|
+
`|--------------|----------|----------|---------------|`
|
|
2192
|
+
];
|
|
2193
|
+
const grade = d.grade ?? "N/A";
|
|
2194
|
+
const gradeColor = grade.startsWith("A") ? "\u2713" : grade.startsWith("B") ? "~" : "\u26A0";
|
|
2195
|
+
lines.push(
|
|
2196
|
+
`| Architecture Coherence | Architecture grade \u2265 B | High | ${gradeColor} ${grade} |`,
|
|
2197
|
+
`| Documentation Coverage | All capabilities documented | Medium | ${d.newDrifts.length === 0 ? "\u2713 No drift" : `\u26A0 ${d.newDrifts.length} drifted`} |`,
|
|
2198
|
+
`| Change Stability | Low churn per capability | Medium | ${d.filesChanged} files changed this period |`
|
|
2199
|
+
);
|
|
2200
|
+
if (d.scoreStart !== null && d.scoreEnd !== null) {
|
|
2201
|
+
const delta = d.scoreDelta !== null ? d.scoreDelta > 0 ? `+${d.scoreDelta}` : String(d.scoreDelta) : "N/A";
|
|
2202
|
+
lines.push(
|
|
2203
|
+
``,
|
|
2204
|
+
`### Coherence Score Trend`,
|
|
2205
|
+
``,
|
|
2206
|
+
`| Period | Start | End | Delta |`,
|
|
2207
|
+
`|--------|-------|-----|-------|`,
|
|
2208
|
+
`| ${d.period} | ${d.scoreStart} | ${d.scoreEnd} | ${delta} |`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
return lines.join("\n");
|
|
2212
|
+
}
|
|
2213
|
+
function buildSection11(opts) {
|
|
2214
|
+
const { digestContext: d, amberContext } = opts;
|
|
2215
|
+
const lines = [
|
|
2216
|
+
`## 11. Risks and Technical Debt`,
|
|
2217
|
+
``,
|
|
2218
|
+
`*Source: PRISM risk signals*`,
|
|
2219
|
+
``,
|
|
2220
|
+
`### Risk Register`,
|
|
2221
|
+
``,
|
|
2222
|
+
`| Risk | Probability | Impact | Mitigation |`,
|
|
2223
|
+
`|------|------------|--------|------------|`
|
|
2224
|
+
];
|
|
2225
|
+
if (d.newRisks.length > 0) {
|
|
2226
|
+
for (const risk of d.newRisks) {
|
|
2227
|
+
lines.push(`| ${risk} | Medium | Medium | Review and address in next sprint |`);
|
|
2228
|
+
}
|
|
2229
|
+
} else {
|
|
2230
|
+
lines.push(`| *No new risks detected in ${d.period}* | \u2014 | \u2014 | \u2014 |`);
|
|
2231
|
+
}
|
|
2232
|
+
if (amberContext && amberContext.driftedCapabilities > 0) {
|
|
2233
|
+
lines.push(`| Documentation drift: ${amberContext.driftedCapabilities} capabilities | High | Low | Run \`prism sync\` |`);
|
|
2234
|
+
}
|
|
2235
|
+
if (d.newDrifts.length > 0) {
|
|
2236
|
+
lines.push(
|
|
2237
|
+
``,
|
|
2238
|
+
`### Technical Debt \u2014 Documentation Drift`,
|
|
2239
|
+
``,
|
|
2240
|
+
`The following capabilities have drifted documentation (code changed since last doc update):`,
|
|
2241
|
+
``,
|
|
2242
|
+
...d.newDrifts.map((c) => `- \`${c}\``)
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
lines.push(
|
|
2246
|
+
``,
|
|
2247
|
+
`### Health Summary`,
|
|
2248
|
+
``,
|
|
2249
|
+
`> ${d.healthSummary}`
|
|
2250
|
+
);
|
|
2251
|
+
return lines.join("\n");
|
|
2252
|
+
}
|
|
2253
|
+
function buildSection12(opts) {
|
|
2254
|
+
const { amberContext, projectName } = opts;
|
|
2255
|
+
const lines = [
|
|
2256
|
+
`## 12. Glossary`,
|
|
2257
|
+
``,
|
|
2258
|
+
`*Source: PRISM AMBER capability registry (Ubiquitous Language)*`,
|
|
2259
|
+
``,
|
|
2260
|
+
`| Term | Definition |`,
|
|
2261
|
+
`|------|------------|`
|
|
2262
|
+
];
|
|
2263
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
2264
|
+
for (const cap of amberContext.capabilities) {
|
|
2265
|
+
const desc = cap.description ? cap.description : `${cap.criticality} ${cap.lifecycle} capability in ${projectName}.`;
|
|
2266
|
+
lines.push(`| **${cap.name}** | ${desc} |`);
|
|
2267
|
+
}
|
|
2268
|
+
} else {
|
|
2269
|
+
lines.push(
|
|
2270
|
+
`| *No capabilities registered yet* | Run an AMBER scan to populate the ubiquitous language. |`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
lines.push(
|
|
2274
|
+
``,
|
|
2275
|
+
`*Add domain-specific terms and acronyms below as the glossary evolves.*`
|
|
2276
|
+
);
|
|
2277
|
+
return lines.join("\n");
|
|
2278
|
+
}
|
|
2279
|
+
function buildSection2() {
|
|
2280
|
+
return `## 2. Constraints
|
|
2281
|
+
|
|
2282
|
+
*Fill in the constraints that apply to your architecture. These are non-negotiable boundaries.*
|
|
2283
|
+
|
|
2284
|
+
### Technical Constraints
|
|
2285
|
+
|
|
2286
|
+
| Constraint | Background |
|
|
2287
|
+
|------------|------------|
|
|
2288
|
+
| *[e.g. Must run on Kubernetes]* | *[reason]* |
|
|
2289
|
+
| *[e.g. Postgres only \u2014 no other DBs]* | *[reason]* |
|
|
2290
|
+
|
|
2291
|
+
### Organizational Constraints
|
|
2292
|
+
|
|
2293
|
+
| Constraint | Background |
|
|
2294
|
+
|------------|------------|
|
|
2295
|
+
| *[e.g. Team of N developers]* | *[reason]* |
|
|
2296
|
+
| *[e.g. 2-week sprints]* | *[reason]* |
|
|
2297
|
+
|
|
2298
|
+
### Conventions
|
|
2299
|
+
|
|
2300
|
+
| Convention | Background |
|
|
2301
|
+
|------------|------------|
|
|
2302
|
+
| *[e.g. TypeScript strict mode]* | *[reason]* |
|
|
2303
|
+
`;
|
|
2304
|
+
}
|
|
2305
|
+
function buildSection6() {
|
|
2306
|
+
return `## 6. Runtime View
|
|
2307
|
+
|
|
2308
|
+
*Describe the important runtime scenarios \u2014 how the system behaves at runtime for key use cases.*
|
|
2309
|
+
|
|
2310
|
+
### Scenario 1: [Name the scenario]
|
|
2311
|
+
|
|
2312
|
+
\`\`\`
|
|
2313
|
+
[Actor] \u2192 [Component A] \u2192 [Component B] \u2192 [External System]
|
|
2314
|
+
\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
|
|
2315
|
+
\`\`\`
|
|
2316
|
+
|
|
2317
|
+
**Description:** *[Describe what happens step by step.]*
|
|
2318
|
+
|
|
2319
|
+
### Scenario 2: [Name the scenario]
|
|
2320
|
+
|
|
2321
|
+
*[Add more scenarios as needed.]*
|
|
2322
|
+
`;
|
|
2323
|
+
}
|
|
2324
|
+
function buildSection7() {
|
|
2325
|
+
return `## 7. Deployment View
|
|
2326
|
+
|
|
2327
|
+
*Describe the technical infrastructure \u2014 environments, nodes, and how the system is distributed.*
|
|
2328
|
+
|
|
2329
|
+
### Infrastructure Level 1
|
|
2330
|
+
|
|
2331
|
+
\`\`\`
|
|
2332
|
+
[Environment: Production]
|
|
2333
|
+
\u2514\u2500\u2500 [Server / Cloud Region]
|
|
2334
|
+
\u251C\u2500\u2500 [Application Tier]
|
|
2335
|
+
\u251C\u2500\u2500 [Database Tier]
|
|
2336
|
+
\u2514\u2500\u2500 [External Services]
|
|
2337
|
+
\`\`\`
|
|
2338
|
+
|
|
2339
|
+
### Environments
|
|
2340
|
+
|
|
2341
|
+
| Environment | Purpose | URL / Access |
|
|
2342
|
+
|-------------|---------|-------------|
|
|
2343
|
+
| Production | Live system | *[URL]* |
|
|
2344
|
+
| Staging | Pre-production testing | *[URL]* |
|
|
2345
|
+
| Development | Local dev | localhost |
|
|
2346
|
+
|
|
2347
|
+
*Fill in your actual infrastructure topology.*
|
|
2348
|
+
`;
|
|
2349
|
+
}
|
|
2350
|
+
function buildLlmContext(opts) {
|
|
2351
|
+
const { projectName, projectDescription, teamSize, techStack, digestContext: d, amberContext } = opts;
|
|
2352
|
+
const lines = [
|
|
2353
|
+
`Project: ${projectName}`,
|
|
2354
|
+
projectDescription ? `Description: ${projectDescription}` : "",
|
|
2355
|
+
teamSize ? `Team size: ${teamSize}` : "",
|
|
2356
|
+
techStack && techStack.length > 0 ? `Tech stack: ${techStack.join(", ")}` : "",
|
|
2357
|
+
``,
|
|
2358
|
+
`Architecture health: ${d.healthSummary}`,
|
|
2359
|
+
d.grade ? `Grade: ${d.grade}` : "",
|
|
2360
|
+
`Period: ${d.period}`,
|
|
2361
|
+
`Commits: ${d.commitCount}`,
|
|
2362
|
+
`Files changed: ${d.filesChanged}`,
|
|
2363
|
+
d.topCommits.length > 0 ? `Recent commits: ${d.topCommits.slice(0, 4).join("; ")}` : ""
|
|
2364
|
+
].filter(Boolean);
|
|
2365
|
+
if (amberContext && amberContext.capabilities.length > 0) {
|
|
2366
|
+
lines.push(
|
|
2367
|
+
``,
|
|
2368
|
+
`Capabilities: ${amberContext.capabilities.map((c) => c.name).join(", ")}`,
|
|
2369
|
+
`Total capabilities: ${amberContext.capabilities.length}`
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
return lines.join("\n");
|
|
2373
|
+
}
|
|
2374
|
+
async function generateLlmSections(opts) {
|
|
2375
|
+
const context = buildLlmContext(opts);
|
|
2376
|
+
const systemPrompt = `You are an expert software architect generating arc42 architecture documentation.
|
|
2377
|
+
Write concise, informative Markdown content for each section.
|
|
2378
|
+
Base your content on the provided PRISM architecture data and project information.
|
|
2379
|
+
Use professional, clear language appropriate for technical documentation.
|
|
2380
|
+
Do not reproduce copyrighted content. All insights are derived from the provided data.`;
|
|
2381
|
+
const userPrompt = `Generate four arc42 documentation sections for this project.
|
|
2382
|
+
|
|
2383
|
+
Project data:
|
|
2384
|
+
${context}
|
|
2385
|
+
|
|
2386
|
+
Generate the following four sections in Markdown. Each section should start with the exact header shown.
|
|
2387
|
+
Keep each section focused and practical \u2014 100\u2013300 words per section.
|
|
2388
|
+
|
|
2389
|
+
---
|
|
2390
|
+
## 1. Introduction and Goals
|
|
2391
|
+
|
|
2392
|
+
Write an overview of:
|
|
2393
|
+
- What the system does (based on the project name, description, and capabilities)
|
|
2394
|
+
- Key quality goals (2\u20133 measurable goals)
|
|
2395
|
+
- Key stakeholders and their expectations
|
|
2396
|
+
|
|
2397
|
+
---
|
|
2398
|
+
## 3. Context and Scope
|
|
2399
|
+
|
|
2400
|
+
Write:
|
|
2401
|
+
- A system context description (what the system does, what it connects to)
|
|
2402
|
+
- External systems and actors that interact with the system
|
|
2403
|
+
- What is explicitly out of scope
|
|
2404
|
+
|
|
2405
|
+
---
|
|
2406
|
+
## 4. Solution Strategy
|
|
2407
|
+
|
|
2408
|
+
Write:
|
|
2409
|
+
- Core technology decisions and why
|
|
2410
|
+
- Key architectural patterns used (infer from the tech stack and capability structure)
|
|
2411
|
+
- How quality goals are addressed by the architecture
|
|
2412
|
+
|
|
2413
|
+
---
|
|
2414
|
+
## 8. Crosscutting Concepts
|
|
2415
|
+
|
|
2416
|
+
Identify crosscutting concerns from the capability names and architecture:
|
|
2417
|
+
- Security / authentication approach
|
|
2418
|
+
- Error handling and logging
|
|
2419
|
+
- Caching strategy (if applicable)
|
|
2420
|
+
- Testing approach
|
|
2421
|
+
- Any other crosscutting patterns visible from the capability structure
|
|
2422
|
+
`;
|
|
2423
|
+
const response = await opts.llm.complete({
|
|
2424
|
+
systemPrompt,
|
|
2425
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
2426
|
+
maxTokens: 4096
|
|
2427
|
+
});
|
|
2428
|
+
const text = response.content;
|
|
2429
|
+
function extractSection(marker, nextMarker) {
|
|
2430
|
+
const start = text.indexOf(marker);
|
|
2431
|
+
if (start === -1) return "";
|
|
2432
|
+
const end = nextMarker ? text.indexOf(nextMarker, start + marker.length) : text.length;
|
|
2433
|
+
return end === -1 ? text.slice(start).trim() : text.slice(start, end).trim();
|
|
2434
|
+
}
|
|
2435
|
+
const s1 = extractSection("## 1. Introduction", "## 3. Context");
|
|
2436
|
+
const s3 = extractSection("## 3. Context", "## 4. Solution");
|
|
2437
|
+
const s4 = extractSection("## 4. Solution", "## 8. Crosscutting");
|
|
2438
|
+
const s8 = extractSection("## 8. Crosscutting", "");
|
|
2439
|
+
return { s1, s3, s4, s8 };
|
|
2440
|
+
}
|
|
2441
|
+
function fallbackSection1(opts) {
|
|
2442
|
+
const { projectName, projectDescription, amberContext } = opts;
|
|
2443
|
+
const capCount = amberContext?.capabilities.length ?? 0;
|
|
2444
|
+
return `## 1. Introduction and Goals
|
|
2445
|
+
|
|
2446
|
+
**${projectName}** ${projectDescription ?? "is a software system documented using arc42."}
|
|
2447
|
+
|
|
2448
|
+
### Quality Goals
|
|
2449
|
+
|
|
2450
|
+
| Priority | Quality Goal | Scenario |
|
|
2451
|
+
|----------|-------------|----------|
|
|
2452
|
+
| 1 | Correctness | System produces correct results |
|
|
2453
|
+
| 2 | Maintainability | New features can be added without regression |
|
|
2454
|
+
| 3 | Performance | System responds within acceptable time |
|
|
2455
|
+
|
|
2456
|
+
### Stakeholders
|
|
2457
|
+
|
|
2458
|
+
| Role | Expectations |
|
|
2459
|
+
|------|-------------|
|
|
2460
|
+
| Development Team | Clear architecture structure, ${capCount} documented capabilities |
|
|
2461
|
+
| Product Owner | Feature delivery aligned with roadmap |
|
|
2462
|
+
| Operations | System is deployable and observable |
|
|
2463
|
+
|
|
2464
|
+
*Generated from PRISM data \u2014 enrich with project-specific goals.*
|
|
2465
|
+
`;
|
|
2466
|
+
}
|
|
2467
|
+
function fallbackSection3(opts) {
|
|
2468
|
+
const { projectName } = opts;
|
|
2469
|
+
return `## 3. Context and Scope
|
|
2470
|
+
|
|
2471
|
+
### System Context
|
|
2472
|
+
|
|
2473
|
+
**${projectName}** interacts with the following external systems:
|
|
2474
|
+
|
|
2475
|
+
| External System | Description | Relationship |
|
|
2476
|
+
|----------------|-------------|-------------|
|
|
2477
|
+
| *[Users / Clients]* | End users of the system | Consumer |
|
|
2478
|
+
| *[External API / Service]* | *[Describe]* | Integration |
|
|
2479
|
+
| *[Database]* | Data persistence | Provider |
|
|
2480
|
+
|
|
2481
|
+
### In Scope
|
|
2482
|
+
|
|
2483
|
+
*[Describe what the system is responsible for.]*
|
|
2484
|
+
|
|
2485
|
+
### Out of Scope
|
|
2486
|
+
|
|
2487
|
+
*[Describe what is deliberately excluded.]*
|
|
2488
|
+
`;
|
|
2489
|
+
}
|
|
2490
|
+
function fallbackSection4(opts) {
|
|
2491
|
+
const { techStack } = opts;
|
|
2492
|
+
const stack = techStack && techStack.length > 0 ? techStack.join(", ") : "TypeScript, Next.js";
|
|
2493
|
+
return `## 4. Solution Strategy
|
|
2494
|
+
|
|
2495
|
+
### Technology Decisions
|
|
2496
|
+
|
|
2497
|
+
**Tech stack:** ${stack}
|
|
2498
|
+
|
|
2499
|
+
| Decision | Rationale |
|
|
2500
|
+
|----------|-----------|
|
|
2501
|
+
| *[Framework choice]* | *[Why this framework]* |
|
|
2502
|
+
| *[Database choice]* | *[Why this DB]* |
|
|
2503
|
+
| *[Architecture pattern]* | *[Why this pattern]* |
|
|
2504
|
+
|
|
2505
|
+
### Architectural Approach
|
|
2506
|
+
|
|
2507
|
+
*Describe the core architectural approach \u2014 e.g. monolith, microservices, event-driven, CQRS.*
|
|
2508
|
+
|
|
2509
|
+
### Quality Goal Mapping
|
|
2510
|
+
|
|
2511
|
+
| Quality Goal | Architecture Response |
|
|
2512
|
+
|-------------|----------------------|
|
|
2513
|
+
| Correctness | *[How architecture ensures correctness]* |
|
|
2514
|
+
| Maintainability | *[How architecture supports maintainability]* |
|
|
2515
|
+
`;
|
|
2516
|
+
}
|
|
2517
|
+
function fallbackSection8(opts) {
|
|
2518
|
+
return `## 8. Crosscutting Concepts
|
|
2519
|
+
|
|
2520
|
+
### Security
|
|
2521
|
+
|
|
2522
|
+
*Describe the authentication and authorization approach.*
|
|
2523
|
+
|
|
2524
|
+
### Error Handling
|
|
2525
|
+
|
|
2526
|
+
*Describe the error handling and logging strategy.*
|
|
2527
|
+
|
|
2528
|
+
### Observability
|
|
2529
|
+
|
|
2530
|
+
*Describe how the system is monitored \u2014 logs, metrics, tracing.*
|
|
2531
|
+
|
|
2532
|
+
### Testing Strategy
|
|
2533
|
+
|
|
2534
|
+
*Describe the testing levels \u2014 unit, integration, end-to-end.*
|
|
2535
|
+
|
|
2536
|
+
### Configuration
|
|
2537
|
+
|
|
2538
|
+
*Describe how the system is configured across environments.*
|
|
2539
|
+
`;
|
|
2540
|
+
}
|
|
2541
|
+
async function generateArc42(opts) {
|
|
2542
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2543
|
+
const s5Content = buildSection5(opts);
|
|
2544
|
+
const s9Content = buildSection9(opts);
|
|
2545
|
+
const s10Content = buildSection10(opts);
|
|
2546
|
+
const s11Content = buildSection11(opts);
|
|
2547
|
+
const s12Content = buildSection12(opts);
|
|
2548
|
+
const s2Content = buildSection2();
|
|
2549
|
+
const s6Content = buildSection6();
|
|
2550
|
+
const s7Content = buildSection7();
|
|
2551
|
+
let s1Content;
|
|
2552
|
+
let s3Content;
|
|
2553
|
+
let s4Content;
|
|
2554
|
+
let s8Content;
|
|
2555
|
+
try {
|
|
2556
|
+
const llmSections = await generateLlmSections(opts);
|
|
2557
|
+
s1Content = llmSections.s1 || fallbackSection1(opts);
|
|
2558
|
+
s3Content = llmSections.s3 || fallbackSection3(opts);
|
|
2559
|
+
s4Content = llmSections.s4 || fallbackSection4(opts);
|
|
2560
|
+
s8Content = llmSections.s8 || fallbackSection8(opts);
|
|
2561
|
+
} catch {
|
|
2562
|
+
s1Content = fallbackSection1(opts);
|
|
2563
|
+
s3Content = fallbackSection3(opts);
|
|
2564
|
+
s4Content = fallbackSection4(opts);
|
|
2565
|
+
s8Content = fallbackSection8();
|
|
2566
|
+
}
|
|
2567
|
+
const sections = [
|
|
2568
|
+
{ number: 1, title: "Introduction and Goals", content: s1Content, dataSource: "llm" },
|
|
2569
|
+
{ number: 2, title: "Constraints", content: s2Content, dataSource: "template" },
|
|
2570
|
+
{ number: 3, title: "Context and Scope", content: s3Content, dataSource: "llm" },
|
|
2571
|
+
{ number: 4, title: "Solution Strategy", content: s4Content, dataSource: "llm" },
|
|
2572
|
+
{ number: 5, title: "Building Block View", content: s5Content, dataSource: "prism" },
|
|
2573
|
+
{ number: 6, title: "Runtime View", content: s6Content, dataSource: "template" },
|
|
2574
|
+
{ number: 7, title: "Deployment View", content: s7Content, dataSource: "template" },
|
|
2575
|
+
{ number: 8, title: "Crosscutting Concepts", content: s8Content, dataSource: "llm" },
|
|
2576
|
+
{ number: 9, title: "Architecture Decisions", content: s9Content, dataSource: "prism" },
|
|
2577
|
+
{ number: 10, title: "Quality Requirements", content: s10Content, dataSource: "prism" },
|
|
2578
|
+
{ number: 11, title: "Risks and Technical Debt", content: s11Content, dataSource: "prism" },
|
|
2579
|
+
{ number: 12, title: "Glossary", content: s12Content, dataSource: "prism" }
|
|
2580
|
+
];
|
|
2581
|
+
const fullMarkdown = [
|
|
2582
|
+
`<!-- This document uses the arc42 template \xA9 arc42.org, licensed under CC-BY 4.0 -->`,
|
|
2583
|
+
`<!-- Generated by PRISM0x2A + forge0x2B \u2014 Architecture intelligence -->`,
|
|
2584
|
+
`<!-- ${generatedAt} -->`,
|
|
2585
|
+
``,
|
|
2586
|
+
`# Architecture Documentation \u2014 ${opts.projectName}`,
|
|
2587
|
+
``,
|
|
2588
|
+
`> ${ARC42_ATTRIBUTION}`,
|
|
2589
|
+
``,
|
|
2590
|
+
...sections.map((s) => s.content)
|
|
2591
|
+
].join("\n\n");
|
|
2592
|
+
return {
|
|
2593
|
+
sections,
|
|
2594
|
+
fullMarkdown,
|
|
2595
|
+
attribution: ARC42_ATTRIBUTION,
|
|
2596
|
+
generatedAt
|
|
2597
|
+
};
|
|
407
2598
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
const guide = FORMAT_GUIDES[format];
|
|
413
|
-
const lengthGuide = {
|
|
414
|
-
short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
|
|
415
|
-
medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
|
|
416
|
-
long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
|
|
417
|
-
}[length];
|
|
418
|
-
const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
|
|
2599
|
+
|
|
2600
|
+
// src/generators/generateKnowledgeCapture.ts
|
|
2601
|
+
function buildContext5(opts) {
|
|
2602
|
+
const { digestContext: d, amberContext, projectName, departingDeveloper, focusCapabilities } = opts;
|
|
419
2603
|
const lines = [
|
|
420
|
-
`##
|
|
421
|
-
|
|
2604
|
+
`## Knowledge Capture Context`,
|
|
2605
|
+
`Project: ${projectName}`,
|
|
2606
|
+
departingDeveloper ? `Departing Developer: ${departingDeveloper}` : `Departing Developer: (not specified \u2014 write for general knowledge transfer)`,
|
|
2607
|
+
focusCapabilities && focusCapabilities.length > 0 ? `Focus Capabilities: ${focusCapabilities.join(", ")}` : `Focus Capabilities: all`,
|
|
422
2608
|
``,
|
|
423
|
-
`##
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
2609
|
+
`## Recent Activity \u2014 ${d.period}`,
|
|
2610
|
+
`Commits: ${d.commitCount}`,
|
|
2611
|
+
`Files changed: ${d.filesChanged}`,
|
|
2612
|
+
`Architecture grade: ${d.grade ?? "N/A"}`,
|
|
2613
|
+
`Health: ${d.healthSummary}`,
|
|
2614
|
+
``
|
|
2615
|
+
];
|
|
2616
|
+
if (d.topCommits.length > 0) {
|
|
2617
|
+
lines.push(`### Recent Commits`, ...d.topCommits.map((c) => `- ${c}`), ``);
|
|
2618
|
+
}
|
|
2619
|
+
if (d.newDrifts.length > 0) {
|
|
2620
|
+
lines.push(
|
|
2621
|
+
`### Drifted Capabilities (need documentation attention)`,
|
|
2622
|
+
...d.newDrifts.map((c) => `- ${c}`),
|
|
2623
|
+
``
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
if (d.newRisks.length > 0) {
|
|
2627
|
+
lines.push(`### Known Risks`, ...d.newRisks.map((r) => `- ${r}`), ``);
|
|
2628
|
+
}
|
|
2629
|
+
if (amberContext) {
|
|
2630
|
+
lines.push(`## AMBER Capability Registry`);
|
|
2631
|
+
lines.push(`Total capabilities: ${amberContext.capabilities.length}`);
|
|
2632
|
+
lines.push(`Drifted: ${amberContext.driftedCapabilities}`);
|
|
2633
|
+
lines.push(`Tagged files: ${amberContext.taggedFiles} / ${amberContext.totalFiles} (${amberContext.taggedPercent}%)`);
|
|
2634
|
+
lines.push(``);
|
|
2635
|
+
const toInclude = focusCapabilities && focusCapabilities.length > 0 ? amberContext.capabilities.filter(
|
|
2636
|
+
(c) => focusCapabilities.some((f) => c.name.toLowerCase().includes(f.toLowerCase()) || c.id.toLowerCase().includes(f.toLowerCase()))
|
|
2637
|
+
) : amberContext.capabilities;
|
|
2638
|
+
if (toInclude.length > 0) {
|
|
2639
|
+
lines.push(`### Capabilities`);
|
|
2640
|
+
for (const cap of toInclude.slice(0, 20)) {
|
|
2641
|
+
lines.push(`#### ${cap.name} (${cap.id})`);
|
|
2642
|
+
lines.push(`- Criticality: ${cap.criticality}`);
|
|
2643
|
+
lines.push(`- Lifecycle: ${cap.lifecycle}`);
|
|
2644
|
+
lines.push(`- Files: ${cap.files.length}`);
|
|
2645
|
+
lines.push(`- Drift: ${cap.driftCount} file${cap.driftCount !== 1 ? "s" : ""} out of date`);
|
|
2646
|
+
if (cap.description) lines.push(`- Description: ${cap.description}`);
|
|
2647
|
+
lines.push(``);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
if (amberContext.orphanedFiles.length > 0) {
|
|
2651
|
+
lines.push(
|
|
2652
|
+
`### Orphaned Files (no capability tag)`,
|
|
2653
|
+
...amberContext.orphanedFiles.slice(0, 10).map((f) => `- ${f}`),
|
|
2654
|
+
``
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return lines.join("\n");
|
|
2659
|
+
}
|
|
2660
|
+
function buildSystemPrompt13(opts) {
|
|
2661
|
+
const devName = opts.departingDeveloper ?? "a departing developer";
|
|
2662
|
+
return [
|
|
2663
|
+
`You are generating a Knowledge Capture document \u2014 a complete architectural brain dump before ${devName} leaves the team.`,
|
|
2664
|
+
`This document is crucial. It preserves institutional knowledge that would otherwise walk out the door.`,
|
|
2665
|
+
`Write with warmth and humanity \u2014 this is someone's legacy. Be thorough, specific, and genuinely useful.`,
|
|
2666
|
+
`Make it feel like ${devName} wrote it themselves, with care for the next person who will maintain this system.`,
|
|
2667
|
+
`Generate ONLY valid JSON \u2014 no markdown fences, no prose, no commentary outside the JSON object.`
|
|
2668
|
+
].join(" ");
|
|
2669
|
+
}
|
|
2670
|
+
function buildUserPrompt13(opts) {
|
|
2671
|
+
const context = buildContext5(opts);
|
|
2672
|
+
const devName = opts.departingDeveloper ?? "the departing developer";
|
|
2673
|
+
return [
|
|
2674
|
+
`Generate a complete Knowledge Capture document for: ${opts.projectName}`,
|
|
2675
|
+
`This preserves ${devName}'s architectural knowledge for the team.`,
|
|
427
2676
|
``,
|
|
428
|
-
|
|
429
|
-
`Target: ${blueprint.targetPath}`,
|
|
430
|
-
`Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
|
|
431
|
-
`Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
|
|
2677
|
+
context,
|
|
432
2678
|
``,
|
|
433
|
-
|
|
434
|
-
|
|
2679
|
+
`## Required Document Structure`,
|
|
2680
|
+
`The document must cover these 6 sections:`,
|
|
2681
|
+
`1. Architecture Overview \u2014 what the system does, how capabilities relate to each other, the mental model`,
|
|
2682
|
+
`2. Critical Capabilities \u2014 deep dive on the most important/drifted/complex capabilities, what makes them tricky`,
|
|
2683
|
+
`3. Known Issues & Workarounds \u2014 things that don't work as expected, temporary hacks, "do not touch" zones`,
|
|
2684
|
+
`4. Where to Start \u2014 recommended reading order for a new developer, which files to read first`,
|
|
2685
|
+
`5. Tribal Knowledge \u2014 things not obvious from code: why decisions were made, what was tried and failed, hidden dependencies`,
|
|
2686
|
+
`6. Onboarding Checklist \u2014 concrete 10-step list for a new team member joining this project`,
|
|
2687
|
+
``,
|
|
2688
|
+
`## Output Format`,
|
|
2689
|
+
`Return a single JSON object with this exact shape:`,
|
|
2690
|
+
`{`,
|
|
2691
|
+
` "sections": [`,
|
|
2692
|
+
` { "title": "<section title>", "content": "<full markdown content for this section, multiple paragraphs>" }`,
|
|
2693
|
+
` ],`,
|
|
2694
|
+
` "criticalKnowledge": ["<most important thing 1>", "<most important thing 2>", ...],`,
|
|
2695
|
+
` "onboardingChecklist": ["<step 1: specific action>", "<step 2: specific action>", ...]`,
|
|
2696
|
+
`}`,
|
|
2697
|
+
``,
|
|
2698
|
+
`Rules:`,
|
|
2699
|
+
`- sections: exactly 6, in the order above, with rich content (not just bullets)`,
|
|
2700
|
+
`- criticalKnowledge: 5-8 bullets \u2014 the things a new developer MUST know or they will make mistakes`,
|
|
2701
|
+
`- onboardingChecklist: exactly 10 items \u2014 concrete, actionable steps (not "read the README", but "read src/auth/ starting with session.ts")`,
|
|
2702
|
+
`- Write as if ${devName} is speaking directly to their replacement \u2014 warm, honest, specific`,
|
|
2703
|
+
`- Include file paths and capability names from the context where relevant`,
|
|
2704
|
+
`- Derive all content from the context \u2014 no invented facts`,
|
|
2705
|
+
`- Return ONLY valid JSON. No markdown fences. No prose outside the JSON.`
|
|
2706
|
+
].join("\n");
|
|
2707
|
+
}
|
|
2708
|
+
function sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist) {
|
|
2709
|
+
const devName = opts.departingDeveloper ?? "the team";
|
|
2710
|
+
const date = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" });
|
|
2711
|
+
const parts = [
|
|
2712
|
+
`# Knowledge Capture: ${opts.projectName}`,
|
|
2713
|
+
``,
|
|
2714
|
+
`> **Prepared by:** ${devName} `,
|
|
2715
|
+
`> **Date:** ${date} `,
|
|
2716
|
+
`> **Purpose:** Preserve architectural knowledge for the next maintainer`,
|
|
2717
|
+
``,
|
|
2718
|
+
`---`,
|
|
2719
|
+
``,
|
|
2720
|
+
`## TL;DR \u2014 Critical Knowledge`,
|
|
2721
|
+
``,
|
|
2722
|
+
...criticalKnowledge.map((k) => `- ${k}`),
|
|
2723
|
+
``,
|
|
2724
|
+
`---`,
|
|
2725
|
+
``
|
|
435
2726
|
];
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
|
|
2727
|
+
for (const section of sections) {
|
|
2728
|
+
parts.push(`## ${section.title}`, ``, section.content, ``, `---`, ``);
|
|
439
2729
|
}
|
|
440
|
-
|
|
441
|
-
|
|
2730
|
+
parts.push(
|
|
2731
|
+
`## Onboarding Checklist`,
|
|
2732
|
+
``,
|
|
2733
|
+
`For the next person joining this project:`,
|
|
2734
|
+
``,
|
|
2735
|
+
...onboardingChecklist.map((step, i) => `- [ ] **Step ${i + 1}:** ${step}`),
|
|
2736
|
+
``,
|
|
2737
|
+
`---`,
|
|
2738
|
+
``,
|
|
2739
|
+
`*Generated with [forge0x2B](https://forge0x2b.dev) \xB7 Architecture intelligence for engineering teams*`
|
|
2740
|
+
);
|
|
2741
|
+
return parts.join("\n");
|
|
442
2742
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
2743
|
+
function buildFallback7(opts) {
|
|
2744
|
+
const { digestContext: d, amberContext, projectName, departingDeveloper } = opts;
|
|
2745
|
+
const critCaps = amberContext?.capabilities.filter((c) => c.criticality === "critical" || c.driftCount > 0).slice(0, 5).map((c) => c.name) ?? [];
|
|
2746
|
+
const sections = [
|
|
2747
|
+
{
|
|
2748
|
+
title: "Architecture Overview",
|
|
2749
|
+
content: `${projectName} is composed of ${amberContext?.capabilities.length ?? "several"} tracked capabilities. ${d.healthSummary}
|
|
2750
|
+
|
|
2751
|
+
The system currently has ${d.filesChanged} recently changed files and a coherence grade of ${d.grade ?? "unknown"}.`
|
|
2752
|
+
},
|
|
2753
|
+
{
|
|
2754
|
+
title: "Critical Capabilities",
|
|
2755
|
+
content: critCaps.length > 0 ? `The following capabilities require the most attention:
|
|
2756
|
+
|
|
2757
|
+
${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.`
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
title: "Known Issues & Workarounds",
|
|
2761
|
+
content: d.newRisks.length > 0 ? `The following risks have been identified:
|
|
2762
|
+
|
|
2763
|
+
${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.`
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
title: "Where to Start",
|
|
2767
|
+
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.
|
|
2768
|
+
|
|
2769
|
+
Focus on capabilities with high drift counts first \u2014 those are where documentation has fallen behind the code.`
|
|
2770
|
+
},
|
|
2771
|
+
{
|
|
2772
|
+
title: "Tribal Knowledge",
|
|
2773
|
+
content: `Key things not obvious from reading the code:
|
|
2774
|
+
|
|
2775
|
+
- The AMBER tags in source files are the source of truth for capability ownership
|
|
2776
|
+
- Drift count indicates documentation debt \u2014 files changed without updating @amber-doc tags
|
|
2777
|
+
- Architecture score changes over time \u2014 check \`.green/\` for trend data`
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
title: "Architecture Decisions",
|
|
2781
|
+
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.`
|
|
2782
|
+
}
|
|
2783
|
+
];
|
|
2784
|
+
const criticalKnowledge = [
|
|
2785
|
+
`The AMBER capability registry (.amber/) is the source of truth for how the codebase is organized`,
|
|
2786
|
+
`Drift count > 0 means documentation hasn't kept pace with code changes \u2014 fix before adding features`,
|
|
2787
|
+
amberContext ? `${amberContext.driftedCapabilities} of ${amberContext.capabilities.length} capabilities currently have drift` : `Check drift count before starting work`,
|
|
2788
|
+
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`,
|
|
2789
|
+
`Architecture grade: ${d.grade ?? "unknown"} \u2014 understand what's driving this before making broad changes`
|
|
2790
|
+
].filter(Boolean);
|
|
2791
|
+
const onboardingChecklist = [
|
|
2792
|
+
`Read this entire document before writing any code`,
|
|
2793
|
+
`Install and run PRISM to get a live architecture view`,
|
|
2794
|
+
`Review .amber/capabilities.md \u2014 understand every capability and its criticality`,
|
|
2795
|
+
`Check .amber/state.json to see which files belong to which capabilities`,
|
|
2796
|
+
`Run the test suite and confirm it passes before making changes`,
|
|
2797
|
+
`Read the top ${Math.min(d.topCommits.length || 5, 5)} recent commits to understand current momentum`,
|
|
2798
|
+
`Review capabilities with drift count > 0 \u2014 read both the code and the docs`,
|
|
2799
|
+
`Set up forge0x2B to get daily Architecture Radio briefings`,
|
|
2800
|
+
`Ask the team about any "do not touch" areas that aren't documented`,
|
|
2801
|
+
`Make your first PR small and well-scoped to verify your understanding`
|
|
2802
|
+
];
|
|
2803
|
+
const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
|
|
2804
|
+
return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
|
|
2805
|
+
}
|
|
2806
|
+
function parseLlmResponse7(raw, opts) {
|
|
2807
|
+
let text = raw.trim();
|
|
2808
|
+
if (text.startsWith("```")) {
|
|
2809
|
+
text = text.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
|
|
449
2810
|
}
|
|
450
|
-
|
|
2811
|
+
let parsed;
|
|
2812
|
+
try {
|
|
2813
|
+
parsed = JSON.parse(text);
|
|
2814
|
+
} catch {
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
const rawSections = Array.isArray(parsed.sections) ? parsed.sections : [];
|
|
2818
|
+
const sections = rawSections.filter((s) => typeof s.title === "string").map((s) => {
|
|
2819
|
+
const item = s;
|
|
451
2820
|
return {
|
|
452
|
-
|
|
453
|
-
|
|
2821
|
+
title: item.title,
|
|
2822
|
+
content: typeof item.content === "string" ? item.content : ""
|
|
454
2823
|
};
|
|
455
|
-
}
|
|
456
|
-
const format = opts.format ?? "markdown";
|
|
457
|
-
const tone = opts.tone ?? "professional";
|
|
458
|
-
const maxTokens = FORMAT_GUIDES[format].maxTokens;
|
|
459
|
-
const response = await provider.complete({
|
|
460
|
-
systemPrompt: buildSystemPrompt6(format, tone),
|
|
461
|
-
messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
|
|
462
|
-
maxTokens
|
|
463
2824
|
});
|
|
464
|
-
return
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
2825
|
+
if (sections.length === 0) return null;
|
|
2826
|
+
const rawCritical = Array.isArray(parsed.criticalKnowledge) ? parsed.criticalKnowledge : [];
|
|
2827
|
+
const criticalKnowledge = rawCritical.filter((k) => typeof k === "string").map((k) => k);
|
|
2828
|
+
const rawChecklist = Array.isArray(parsed.onboardingChecklist) ? parsed.onboardingChecklist : [];
|
|
2829
|
+
const onboardingChecklist = rawChecklist.filter((k) => typeof k === "string").map((k) => k);
|
|
2830
|
+
const markdownDoc = sectionsToMarkdown(sections, opts, criticalKnowledge, onboardingChecklist);
|
|
2831
|
+
return { markdownDoc, sections, criticalKnowledge, onboardingChecklist };
|
|
2832
|
+
}
|
|
2833
|
+
async function generateKnowledgeCapture(options) {
|
|
2834
|
+
try {
|
|
2835
|
+
const response = await options.llm.complete({
|
|
2836
|
+
systemPrompt: buildSystemPrompt13(options),
|
|
2837
|
+
messages: [{ role: "user", content: buildUserPrompt13(options) }],
|
|
2838
|
+
maxTokens: 8192
|
|
2839
|
+
});
|
|
2840
|
+
const parsed = parseLlmResponse7(response.content, options);
|
|
2841
|
+
if (parsed) return parsed;
|
|
2842
|
+
} catch {
|
|
2843
|
+
}
|
|
2844
|
+
return buildFallback7(options);
|
|
468
2845
|
}
|
|
469
2846
|
|
|
470
2847
|
// src/forge/types.ts
|
|
@@ -1007,6 +3384,8 @@ function refineLimitState(usedRefines, softCap = REFINE_SESSION_SOFT_CAP) {
|
|
|
1007
3384
|
showCountdown: used >= REFINE_COUNTDOWN_THRESHOLD
|
|
1008
3385
|
};
|
|
1009
3386
|
}
|
|
3387
|
+
|
|
3388
|
+
// src/forge/brandKit.ts
|
|
1010
3389
|
var DEFAULT_BRAND_KIT_PALETTE = {
|
|
1011
3390
|
primary: "#f97316",
|
|
1012
3391
|
secondary: "#1e293b",
|
|
@@ -1150,60 +3529,6 @@ function parseBrandKitFromLlmResponse(text, hints) {
|
|
|
1150
3529
|
prism_detected: false
|
|
1151
3530
|
};
|
|
1152
3531
|
}
|
|
1153
|
-
function deriveTechnicalScore(categories, total) {
|
|
1154
|
-
if (total === 0) return 50;
|
|
1155
|
-
const techFiles = (categories.lib ?? 0) + (categories.hook ?? 0);
|
|
1156
|
-
const appFiles = (categories.app ?? 0) + (categories.component ?? 0);
|
|
1157
|
-
const techRatio = techFiles / total;
|
|
1158
|
-
const appRatio = appFiles / total;
|
|
1159
|
-
return Math.round(30 + techRatio * 50 - appRatio * 10);
|
|
1160
|
-
}
|
|
1161
|
-
function deriveAudienceMix(categories, paths) {
|
|
1162
|
-
const hasAdminPaths = paths.some((p) => /admin|dashboard|ops/.test(p));
|
|
1163
|
-
const hasApiPaths = paths.some((p) => /\/api\/|route\.ts/.test(p));
|
|
1164
|
-
const hasFrontend = (categories.component ?? 0) > 5;
|
|
1165
|
-
if (hasAdminPaths && hasApiPaths) return "developer+operator";
|
|
1166
|
-
if (hasApiPaths && !hasFrontend) return "developer";
|
|
1167
|
-
if (hasFrontend && !hasApiPaths) return "end-user";
|
|
1168
|
-
return "developer+business";
|
|
1169
|
-
}
|
|
1170
|
-
async function extractBrandFromPrismBlueprint(targetPath) {
|
|
1171
|
-
const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
|
|
1172
|
-
let blueprint = {};
|
|
1173
|
-
try {
|
|
1174
|
-
const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
|
|
1175
|
-
blueprint = JSON.parse(raw);
|
|
1176
|
-
} catch {
|
|
1177
|
-
}
|
|
1178
|
-
const categories = blueprint.categories ?? {};
|
|
1179
|
-
const files = blueprint.files ?? [];
|
|
1180
|
-
const paths = files.map((f) => f.path ?? "");
|
|
1181
|
-
const total = blueprint.stats?.totalFiles ?? files.length;
|
|
1182
|
-
const technicalScore = deriveTechnicalScore(categories, total);
|
|
1183
|
-
const audienceMix = deriveAudienceMix(categories, paths);
|
|
1184
|
-
const projectName = blueprint.targetPath ? path__default.default.basename(blueprint.targetPath) : "Project";
|
|
1185
|
-
const formality = technicalScore > 60 ? 65 : 50;
|
|
1186
|
-
const technicality = Math.min(95, technicalScore + 10);
|
|
1187
|
-
return {
|
|
1188
|
-
suggestedName: projectName,
|
|
1189
|
-
voice: {
|
|
1190
|
-
tone: technicalScore > 60 ? "technical" : "professional",
|
|
1191
|
-
audience: audienceMix.includes("developer") ? "developers" : "business",
|
|
1192
|
-
vocabulary: [],
|
|
1193
|
-
avoid: [],
|
|
1194
|
-
formality,
|
|
1195
|
-
technicality
|
|
1196
|
-
},
|
|
1197
|
-
prism_detected: true,
|
|
1198
|
-
blueprint_path: snapshotPath,
|
|
1199
|
-
stats: {
|
|
1200
|
-
totalFiles: total,
|
|
1201
|
-
categories,
|
|
1202
|
-
technicalScore,
|
|
1203
|
-
audienceMix
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
}
|
|
1207
3532
|
|
|
1208
3533
|
// src/forge/brandPalette.ts
|
|
1209
3534
|
function isNonEmptyString(v) {
|
|
@@ -1443,63 +3768,63 @@ function initialFormValues(fields, source) {
|
|
|
1443
3768
|
function isEmptyString(v) {
|
|
1444
3769
|
return typeof v === "string" && v.trim().length === 0;
|
|
1445
3770
|
}
|
|
1446
|
-
function validateField(field, value,
|
|
3771
|
+
function validateField(field, value, path) {
|
|
1447
3772
|
const errors = {};
|
|
1448
3773
|
switch (field.kind) {
|
|
1449
3774
|
case "string": {
|
|
1450
3775
|
if (field.required && (value === void 0 || value === null || isEmptyString(value))) {
|
|
1451
|
-
errors[
|
|
3776
|
+
errors[path] = `${field.label} ist erforderlich`;
|
|
1452
3777
|
break;
|
|
1453
3778
|
}
|
|
1454
3779
|
if (typeof value === "string") {
|
|
1455
3780
|
if (field.minLength !== void 0 && value.length < field.minLength) {
|
|
1456
|
-
errors[
|
|
3781
|
+
errors[path] = `${field.label} mindestens ${field.minLength} Zeichen`;
|
|
1457
3782
|
} else if (field.maxLength !== void 0 && value.length > field.maxLength) {
|
|
1458
|
-
errors[
|
|
3783
|
+
errors[path] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
|
|
1459
3784
|
} else if (field.enum && value.length > 0 && !field.enum.includes(value)) {
|
|
1460
|
-
errors[
|
|
3785
|
+
errors[path] = `${field.label}: Wert nicht in der Auswahl`;
|
|
1461
3786
|
}
|
|
1462
3787
|
}
|
|
1463
3788
|
break;
|
|
1464
3789
|
}
|
|
1465
3790
|
case "number": {
|
|
1466
3791
|
if (value === "" || value === void 0 || value === null) {
|
|
1467
|
-
if (field.required) errors[
|
|
3792
|
+
if (field.required) errors[path] = `${field.label} ist erforderlich`;
|
|
1468
3793
|
break;
|
|
1469
3794
|
}
|
|
1470
3795
|
const n = typeof value === "number" ? value : Number(value);
|
|
1471
3796
|
if (!Number.isFinite(n)) {
|
|
1472
|
-
errors[
|
|
3797
|
+
errors[path] = `${field.label} muss eine Zahl sein`;
|
|
1473
3798
|
} else if (field.integer && !Number.isInteger(n)) {
|
|
1474
|
-
errors[
|
|
3799
|
+
errors[path] = `${field.label} muss eine ganze Zahl sein`;
|
|
1475
3800
|
} else if (field.minimum !== void 0 && n < field.minimum) {
|
|
1476
|
-
errors[
|
|
3801
|
+
errors[path] = `${field.label} >= ${field.minimum}`;
|
|
1477
3802
|
} else if (field.maximum !== void 0 && n > field.maximum) {
|
|
1478
|
-
errors[
|
|
3803
|
+
errors[path] = `${field.label} <= ${field.maximum}`;
|
|
1479
3804
|
}
|
|
1480
3805
|
break;
|
|
1481
3806
|
}
|
|
1482
3807
|
case "boolean":
|
|
1483
3808
|
if (field.required && value !== true) {
|
|
1484
|
-
errors[
|
|
3809
|
+
errors[path] = `${field.label} muss aktiviert sein`;
|
|
1485
3810
|
}
|
|
1486
3811
|
break;
|
|
1487
3812
|
case "array": {
|
|
1488
3813
|
const arr = Array.isArray(value) ? value : [];
|
|
1489
3814
|
if (field.required && arr.length === 0) {
|
|
1490
|
-
errors[
|
|
3815
|
+
errors[path] = `${field.label}: mindestens ein Eintrag erforderlich`;
|
|
1491
3816
|
}
|
|
1492
3817
|
if (field.minItems !== void 0 && arr.length < field.minItems) {
|
|
1493
|
-
errors[
|
|
3818
|
+
errors[path] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
|
|
1494
3819
|
} else if (field.maxItems !== void 0 && arr.length > field.maxItems) {
|
|
1495
|
-
errors[
|
|
3820
|
+
errors[path] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
|
|
1496
3821
|
}
|
|
1497
3822
|
if (field.itemField) {
|
|
1498
3823
|
arr.forEach((item, i) => {
|
|
1499
3824
|
const sub = validateField(
|
|
1500
3825
|
field.itemField,
|
|
1501
3826
|
item,
|
|
1502
|
-
`${
|
|
3827
|
+
`${path}[${i}]`
|
|
1503
3828
|
);
|
|
1504
3829
|
Object.assign(errors, sub);
|
|
1505
3830
|
});
|
|
@@ -1509,7 +3834,7 @@ function validateField(field, value, path3) {
|
|
|
1509
3834
|
case "object": {
|
|
1510
3835
|
const obj = isPlainObject5(value) ? value : {};
|
|
1511
3836
|
for (const sub of field.fields) {
|
|
1512
|
-
const subErrors = validateField(sub, obj[sub.name], `${
|
|
3837
|
+
const subErrors = validateField(sub, obj[sub.name], `${path}.${sub.name}`);
|
|
1513
3838
|
Object.assign(errors, subErrors);
|
|
1514
3839
|
}
|
|
1515
3840
|
break;
|
|
@@ -2004,6 +4329,80 @@ var WIDGET_TEMPLATE_SOCIAL_PROOF = {
|
|
|
2004
4329
|
{ id: "show_divider", label: "Show divider", type: "select", options: ["yes", "no"], default: "no" }
|
|
2005
4330
|
]
|
|
2006
4331
|
};
|
|
4332
|
+
var WIDGET_TEMPLATE_ANIMATED_STAT = {
|
|
4333
|
+
id: "animated-stat",
|
|
4334
|
+
name: "Animated Stat",
|
|
4335
|
+
description: "Full-card animated number counter. Perfect for LinkedIn/Instagram. Export as video or GIF.",
|
|
4336
|
+
free_tier: false,
|
|
4337
|
+
animated: true,
|
|
4338
|
+
animDurationMs: 2200,
|
|
4339
|
+
videoDurationMs: 3800,
|
|
4340
|
+
defaultWidth: 600,
|
|
4341
|
+
defaultHeight: 400,
|
|
4342
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
4343
|
+
slots: [
|
|
4344
|
+
{ id: "value", label: "Value (number)", type: "number", required: true, placeholder: "138", default: "138" },
|
|
4345
|
+
{ id: "label", label: "Label", type: "text", required: true, placeholder: "Files analysed", default: "Files analysed" },
|
|
4346
|
+
{ id: "unit", label: "Unit (optional)", type: "text", placeholder: "k LOC", default: "" },
|
|
4347
|
+
{ id: "sublabel", label: "Sublabel / context", type: "text", placeholder: "across 3 modules", default: "" },
|
|
4348
|
+
{ id: "delta", label: "Delta text", type: "text", placeholder: "+22% this sprint", default: "" },
|
|
4349
|
+
{ id: "delta_direction", label: "Delta direction", type: "select", options: ["up", "down", "neutral"], default: "up" }
|
|
4350
|
+
]
|
|
4351
|
+
};
|
|
4352
|
+
var WIDGET_TEMPLATE_RELEASE_CARD = {
|
|
4353
|
+
id: "release-card",
|
|
4354
|
+
name: "Release Card",
|
|
4355
|
+
description: "Animated release announcement \u2014 version, headline, and staggered change list. Great for LinkedIn posts.",
|
|
4356
|
+
free_tier: false,
|
|
4357
|
+
animated: true,
|
|
4358
|
+
animDurationMs: 1200,
|
|
4359
|
+
videoDurationMs: 3e3,
|
|
4360
|
+
defaultWidth: 600,
|
|
4361
|
+
defaultHeight: 400,
|
|
4362
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
4363
|
+
slots: [
|
|
4364
|
+
{ id: "version", label: "Version", type: "text", required: true, placeholder: "v2.4.0", default: "v2.4.0" },
|
|
4365
|
+
{ id: "headline", label: "Headline", type: "text", required: true, placeholder: "Faster. Smarter. Leaner.", default: "Faster. Smarter. Leaner." },
|
|
4366
|
+
{ 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" },
|
|
4367
|
+
{ id: "tag", label: "Release tag", type: "select", options: ["feat", "fix", "perf", "break", ""], default: "feat" }
|
|
4368
|
+
]
|
|
4369
|
+
};
|
|
4370
|
+
var WIDGET_TEMPLATE_CHART_BARS = {
|
|
4371
|
+
id: "chart-bars",
|
|
4372
|
+
name: "Chart \u2014 Bars",
|
|
4373
|
+
description: "Animated Chart.js bar chart from your codebase metrics. Perfect for visualising churn, coverage, or LOC.",
|
|
4374
|
+
free_tier: false,
|
|
4375
|
+
animated: true,
|
|
4376
|
+
animDurationMs: 1600,
|
|
4377
|
+
videoDurationMs: 3200,
|
|
4378
|
+
defaultWidth: 640,
|
|
4379
|
+
defaultHeight: 420,
|
|
4380
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
4381
|
+
slots: [
|
|
4382
|
+
{ id: "title", label: "Chart title", type: "text", placeholder: "Files by module", default: "Files by module" },
|
|
4383
|
+
{ id: "labels", label: "Labels (one per line)", type: "multiline", required: true, placeholder: "api\nlib\ncomponents\napp", default: "api\nlib\ncomponents\napp" },
|
|
4384
|
+
{ id: "values", label: "Values (one per line)", type: "multiline", required: true, placeholder: "24\n18\n41\n12", default: "24\n18\n41\n12" },
|
|
4385
|
+
{ id: "orientation", label: "Orientation", type: "select", options: ["vertical", "horizontal"], default: "vertical" },
|
|
4386
|
+
{ id: "accent_color", label: "Bar colour (CSS)", type: "color", placeholder: "#f97316", default: "" }
|
|
4387
|
+
]
|
|
4388
|
+
};
|
|
4389
|
+
var WIDGET_TEMPLATE_ARCHITECTURE_BADGE = {
|
|
4390
|
+
id: "architecture-badge",
|
|
4391
|
+
name: "Architecture Badge",
|
|
4392
|
+
description: "Animated tech-stack showcase with Lucide icons. Items pop in with spring animation \u2014 great for profile posts.",
|
|
4393
|
+
free_tier: false,
|
|
4394
|
+
animated: true,
|
|
4395
|
+
animDurationMs: 900,
|
|
4396
|
+
videoDurationMs: 2500,
|
|
4397
|
+
defaultWidth: 580,
|
|
4398
|
+
defaultHeight: 360,
|
|
4399
|
+
exportFormats: ["standalone", "html", "markdown"],
|
|
4400
|
+
slots: [
|
|
4401
|
+
{ id: "title", label: "Title", type: "text", placeholder: "Built with", default: "Built with" },
|
|
4402
|
+
{ id: "items", label: "Stack items (one per line)", type: "multiline", required: true, placeholder: "TypeScript\nNext.js\nPostgres\nTailwind", default: "TypeScript\nNext.js\nPostgres\nTailwind" },
|
|
4403
|
+
{ id: "subtitle", label: "Subtitle / tagline", type: "text", placeholder: "100% local-first \xB7 zero telemetry", default: "" }
|
|
4404
|
+
]
|
|
4405
|
+
};
|
|
2007
4406
|
var BUNDLED_WIDGET_TEMPLATES = [
|
|
2008
4407
|
WIDGET_TEMPLATE_STAT_CARD,
|
|
2009
4408
|
WIDGET_TEMPLATE_FEATURE_GRID,
|
|
@@ -2012,7 +4411,12 @@ var BUNDLED_WIDGET_TEMPLATES = [
|
|
|
2012
4411
|
WIDGET_TEMPLATE_METRIC_BADGE,
|
|
2013
4412
|
WIDGET_TEMPLATE_PRICING_TIER,
|
|
2014
4413
|
WIDGET_TEMPLATE_CHANGELOG_ROW,
|
|
2015
|
-
WIDGET_TEMPLATE_SOCIAL_PROOF
|
|
4414
|
+
WIDGET_TEMPLATE_SOCIAL_PROOF,
|
|
4415
|
+
// Animated — standalone + video export
|
|
4416
|
+
WIDGET_TEMPLATE_ANIMATED_STAT,
|
|
4417
|
+
WIDGET_TEMPLATE_RELEASE_CARD,
|
|
4418
|
+
WIDGET_TEMPLATE_CHART_BARS,
|
|
4419
|
+
WIDGET_TEMPLATE_ARCHITECTURE_BADGE
|
|
2016
4420
|
];
|
|
2017
4421
|
var FREE_TIER_WIDGET_IDS = BUNDLED_WIDGET_TEMPLATES.filter((t) => t.free_tier).map((t) => t.id);
|
|
2018
4422
|
function getWidgetTemplate(id) {
|
|
@@ -2171,6 +4575,184 @@ ${divider ? `<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);m
|
|
|
2171
4575
|
${logos.map((l) => ` <span style="font-weight:600;font-size:.875rem;color:var(--pw-brand-muted);letter-spacing:-.01em">${escHtml(l)}</span>`).join("\n")}
|
|
2172
4576
|
</div>`;
|
|
2173
4577
|
}
|
|
4578
|
+
function renderAnimatedStatHtml(slots, t) {
|
|
4579
|
+
const value = getSlotValue(slots, t, "value");
|
|
4580
|
+
const label = getSlotValue(slots, t, "label");
|
|
4581
|
+
const unit = getSlotValue(slots, t, "unit");
|
|
4582
|
+
const sublabel = getSlotValue(slots, t, "sublabel");
|
|
4583
|
+
const dir = getSlotValue(slots, t, "delta_direction");
|
|
4584
|
+
const delta = getSlotValue(slots, t, "delta");
|
|
4585
|
+
const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
|
|
4586
|
+
const deltaColor = dir === "up" ? "#22c55e" : dir === "down" ? "#ef4444" : "var(--pw-brand-muted)";
|
|
4587
|
+
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)">
|
|
4588
|
+
<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>
|
|
4589
|
+
${unit ? `<div style="font-size:1.25rem;font-weight:600;color:var(--pw-brand-muted);margin-top:-var(--pw-spacing-sm)">${escHtml(unit)}</div>` : ""}
|
|
4590
|
+
<div style="font-size:1.1rem;font-weight:600;color:var(--pw-brand-text);letter-spacing:0.01em">${escHtml(label)}</div>
|
|
4591
|
+
${sublabel ? `<div style="font-size:.8rem;color:var(--pw-brand-muted)">${escHtml(sublabel)}</div>` : ""}
|
|
4592
|
+
${delta ? `<div style="font-size:.875rem;color:${deltaColor};font-weight:600">${arrow}${arrow ? " " : ""}${escHtml(delta)}</div>` : ""}
|
|
4593
|
+
</div>
|
|
4594
|
+
<script>
|
|
4595
|
+
(function(){
|
|
4596
|
+
var target = ${Number(value.replace(/[^0-9.-]/g, "")) || 0};
|
|
4597
|
+
var isFloat = target % 1 !== 0;
|
|
4598
|
+
var obj = { val: 0 };
|
|
4599
|
+
anime({
|
|
4600
|
+
targets: obj,
|
|
4601
|
+
val: target,
|
|
4602
|
+
duration: ${2e3},
|
|
4603
|
+
easing: "easeOutExpo",
|
|
4604
|
+
update: function() {
|
|
4605
|
+
document.getElementById("pw-anim-value").textContent =
|
|
4606
|
+
isFloat ? obj.val.toFixed(1) : Math.round(obj.val).toLocaleString();
|
|
4607
|
+
}
|
|
4608
|
+
});
|
|
4609
|
+
})();
|
|
4610
|
+
</script>`;
|
|
4611
|
+
}
|
|
4612
|
+
function renderReleaseCardHtml(slots, t) {
|
|
4613
|
+
const version = getSlotValue(slots, t, "version");
|
|
4614
|
+
const headline = getSlotValue(slots, t, "headline");
|
|
4615
|
+
const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
|
|
4616
|
+
const tag = getSlotValue(slots, t, "tag");
|
|
4617
|
+
const tagColors = { feat: "#22c55e", fix: "#f59e0b", perf: "#60a5fa", break: "#ef4444" };
|
|
4618
|
+
const tc = tagColors[tag] ?? "var(--pw-brand-accent)";
|
|
4619
|
+
return `<div class="pw-release-wrap" style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
|
|
4620
|
+
<div style="display:flex;align-items:center;gap:var(--pw-spacing-sm);opacity:0" class="pw-ri">
|
|
4621
|
+
<span style="font-family:var(--pw-font-mono);font-size:1.1rem;font-weight:700;color:var(--pw-brand-primary)">${escHtml(version)}</span>
|
|
4622
|
+
${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>` : ""}
|
|
4623
|
+
</div>
|
|
4624
|
+
<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>
|
|
4625
|
+
<ul style="margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:var(--pw-spacing-xs)">
|
|
4626
|
+
${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 ")}
|
|
4627
|
+
</ul>
|
|
4628
|
+
</div>
|
|
4629
|
+
<script>
|
|
4630
|
+
(function(){
|
|
4631
|
+
anime({ targets: ".pw-ri", translateY: [-16, 0], opacity: [0, 1], delay: anime.stagger(120), duration: 500, easing: "easeOutCubic" });
|
|
4632
|
+
anime({ targets: ".pw-change-item", translateX: [-20, 0], opacity: [0, 1], delay: anime.stagger(80, { start: 350 }), duration: 450, easing: "easeOutCubic" });
|
|
4633
|
+
})();
|
|
4634
|
+
</script>`;
|
|
4635
|
+
}
|
|
4636
|
+
function renderChartBarsHtml(slots, t) {
|
|
4637
|
+
const title = getSlotValue(slots, t, "title");
|
|
4638
|
+
const labelsRaw = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
|
|
4639
|
+
const valuesRaw = getSlotValue(slots, t, "values").split("\n").filter(Boolean).map(Number);
|
|
4640
|
+
const orientation = getSlotValue(slots, t, "orientation") || "vertical";
|
|
4641
|
+
getSlotValue(slots, t, "accent_color") || "var(--pw-brand-primary)";
|
|
4642
|
+
const labelsJson = JSON.stringify(labelsRaw);
|
|
4643
|
+
const valuesJson = JSON.stringify(valuesRaw);
|
|
4644
|
+
const chartType = orientation === "horizontal" ? "bar" : "bar";
|
|
4645
|
+
const indexAxis = orientation === "horizontal" ? `indexAxis: "y",` : "";
|
|
4646
|
+
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>` : ""}
|
|
4647
|
+
<div style="position:relative;width:100%;height:220px">
|
|
4648
|
+
<canvas id="pw-chart" style="width:100%;height:100%"></canvas>
|
|
4649
|
+
</div>
|
|
4650
|
+
<script>
|
|
4651
|
+
(function(){
|
|
4652
|
+
var ctx = document.getElementById("pw-chart").getContext("2d");
|
|
4653
|
+
var accent = getComputedStyle(document.querySelector(".pw-widget")).getPropertyValue("--pw-brand-primary").trim() || "#f97316";
|
|
4654
|
+
new Chart(ctx, {
|
|
4655
|
+
type: "${chartType}",
|
|
4656
|
+
data: {
|
|
4657
|
+
labels: ${labelsJson},
|
|
4658
|
+
datasets: [{
|
|
4659
|
+
data: ${valuesJson},
|
|
4660
|
+
backgroundColor: accent + "cc",
|
|
4661
|
+
borderColor: accent,
|
|
4662
|
+
borderWidth: 2,
|
|
4663
|
+
borderRadius: 6,
|
|
4664
|
+
}]
|
|
4665
|
+
},
|
|
4666
|
+
options: {
|
|
4667
|
+
${indexAxis}
|
|
4668
|
+
responsive: true,
|
|
4669
|
+
maintainAspectRatio: false,
|
|
4670
|
+
animation: { duration: 1500, easing: "easeOutQuart" },
|
|
4671
|
+
plugins: { legend: { display: false } },
|
|
4672
|
+
scales: {
|
|
4673
|
+
x: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } },
|
|
4674
|
+
y: { grid: { color: "rgba(255,255,255,.06)" }, ticks: { color: "rgba(255,255,255,.5)", font: { size: 11 } } }
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
});
|
|
4678
|
+
})();
|
|
4679
|
+
</script>`;
|
|
4680
|
+
}
|
|
4681
|
+
var LUCIDE_ICON_MAP = {
|
|
4682
|
+
typescript: "code-2",
|
|
4683
|
+
javascript: "code-2",
|
|
4684
|
+
"js": "code-2",
|
|
4685
|
+
"ts": "code-2",
|
|
4686
|
+
react: "atom",
|
|
4687
|
+
next: "triangle",
|
|
4688
|
+
nextjs: "triangle",
|
|
4689
|
+
"next.js": "triangle",
|
|
4690
|
+
node: "server",
|
|
4691
|
+
nodejs: "server",
|
|
4692
|
+
"node.js": "server",
|
|
4693
|
+
postgres: "database",
|
|
4694
|
+
postgresql: "database",
|
|
4695
|
+
mysql: "database",
|
|
4696
|
+
sqlite: "database",
|
|
4697
|
+
supabase: "database",
|
|
4698
|
+
prisma: "layers",
|
|
4699
|
+
tailwind: "palette",
|
|
4700
|
+
css: "palette",
|
|
4701
|
+
sass: "palette",
|
|
4702
|
+
docker: "box",
|
|
4703
|
+
kubernetes: "cloud",
|
|
4704
|
+
aws: "cloud",
|
|
4705
|
+
vercel: "triangle",
|
|
4706
|
+
github: "git-branch",
|
|
4707
|
+
git: "git-branch",
|
|
4708
|
+
graphql: "share-2",
|
|
4709
|
+
rest: "globe",
|
|
4710
|
+
api: "globe",
|
|
4711
|
+
redis: "zap",
|
|
4712
|
+
kafka: "radio",
|
|
4713
|
+
python: "terminal",
|
|
4714
|
+
rust: "cpu",
|
|
4715
|
+
go: "activity",
|
|
4716
|
+
java: "coffee",
|
|
4717
|
+
vue: "layers",
|
|
4718
|
+
svelte: "layers",
|
|
4719
|
+
angular: "layers",
|
|
4720
|
+
figma: "pen-tool",
|
|
4721
|
+
linear: "layers",
|
|
4722
|
+
default: "layers"
|
|
4723
|
+
};
|
|
4724
|
+
function renderArchitectureBadgeHtml(slots, t) {
|
|
4725
|
+
const title = getSlotValue(slots, t, "title");
|
|
4726
|
+
const subtitle = getSlotValue(slots, t, "subtitle");
|
|
4727
|
+
const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
|
|
4728
|
+
const palette = ["#60a5fa", "#f97316", "#34d399", "#a78bfa", "#fb923c", "#38bdf8", "#facc15", "#f472b6"];
|
|
4729
|
+
const chips = items.map((item, i) => {
|
|
4730
|
+
const key = item.trim().toLowerCase().replace(/[^a-z0-9.]/g, "");
|
|
4731
|
+
const icon = LUCIDE_ICON_MAP[key] ?? LUCIDE_ICON_MAP.default;
|
|
4732
|
+
const color = palette[i % palette.length];
|
|
4733
|
+
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">
|
|
4734
|
+
<i data-lucide="${icon}" style="width:14px;height:14px;color:${color};flex-shrink:0"></i>
|
|
4735
|
+
<span style="font-size:.8rem;font-weight:600;color:${color};letter-spacing:.02em">${escHtml(item.trim())}</span>
|
|
4736
|
+
</div>`;
|
|
4737
|
+
}).join("\n ");
|
|
4738
|
+
return `<div style="display:flex;flex-direction:column;gap:var(--pw-spacing-md)">
|
|
4739
|
+
${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>` : ""}
|
|
4740
|
+
<div style="display:flex;flex-wrap:wrap;gap:var(--pw-spacing-xs)">
|
|
4741
|
+
${chips}
|
|
4742
|
+
</div>
|
|
4743
|
+
${subtitle ? `<p style="margin:0;font-size:.75rem;color:var(--pw-brand-muted);opacity:0" class="pw-arch-sub">${escHtml(subtitle)}</p>` : ""}
|
|
4744
|
+
</div>
|
|
4745
|
+
<script>
|
|
4746
|
+
(function(){
|
|
4747
|
+
lucide.createIcons();
|
|
4748
|
+
var title = document.getElementById("pw-arch-title");
|
|
4749
|
+
if(title) anime({ targets: title, opacity: [0,1], translateY: [-10,0], duration: 400, easing: "easeOutCubic" });
|
|
4750
|
+
anime({ targets: ".pw-arch-item", opacity: [0,1], scale: [0.85,1], delay: anime.stagger(70, { start: 200 }), duration: 400, easing: "easeOutBack" });
|
|
4751
|
+
var sub = document.querySelector(".pw-arch-sub");
|
|
4752
|
+
if(sub) anime({ targets: sub, opacity: [0,1], delay: 600, duration: 400, easing: "easeOutCubic" });
|
|
4753
|
+
})();
|
|
4754
|
+
</script>`;
|
|
4755
|
+
}
|
|
2174
4756
|
var HTML_BODY_FN = {
|
|
2175
4757
|
"stat-card": renderStatCardHtml,
|
|
2176
4758
|
"feature-grid": renderFeatureGridHtml,
|
|
@@ -2179,7 +4761,11 @@ var HTML_BODY_FN = {
|
|
|
2179
4761
|
"metric-badge": renderMetricBadgeHtml,
|
|
2180
4762
|
"pricing-tier": renderPricingTierHtml,
|
|
2181
4763
|
"changelog-row": renderChangelogRowHtml,
|
|
2182
|
-
"social-proof": renderSocialProofHtml
|
|
4764
|
+
"social-proof": renderSocialProofHtml,
|
|
4765
|
+
"animated-stat": renderAnimatedStatHtml,
|
|
4766
|
+
"release-card": renderReleaseCardHtml,
|
|
4767
|
+
"chart-bars": renderChartBarsHtml,
|
|
4768
|
+
"architecture-badge": renderArchitectureBadgeHtml
|
|
2183
4769
|
};
|
|
2184
4770
|
function renderStatCardMd(slots, t) {
|
|
2185
4771
|
const value = getSlotValue(slots, t, "value");
|
|
@@ -2250,6 +4836,43 @@ function renderSocialProofMd(slots, t) {
|
|
|
2250
4836
|
const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
|
|
2251
4837
|
return `${caption ? caption + "\n\n" : ""}${logos.join(" \xB7 ")}`;
|
|
2252
4838
|
}
|
|
4839
|
+
function renderAnimatedStatMd(slots, t) {
|
|
4840
|
+
const value = getSlotValue(slots, t, "value");
|
|
4841
|
+
const label = getSlotValue(slots, t, "label");
|
|
4842
|
+
const unit = getSlotValue(slots, t, "unit");
|
|
4843
|
+
const delta = getSlotValue(slots, t, "delta");
|
|
4844
|
+
const dir = getSlotValue(slots, t, "delta_direction");
|
|
4845
|
+
const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
|
|
4846
|
+
return `**${value}${unit ? " " + unit : ""}** \u2014 ${label}${delta ? ` ${arrow} ${delta}` : ""}`;
|
|
4847
|
+
}
|
|
4848
|
+
function renderReleaseCardMd(slots, t) {
|
|
4849
|
+
const version = getSlotValue(slots, t, "version");
|
|
4850
|
+
const headline = getSlotValue(slots, t, "headline");
|
|
4851
|
+
const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
|
|
4852
|
+
const tag = getSlotValue(slots, t, "tag");
|
|
4853
|
+
return `### ${version}${tag ? ` \`${tag}\`` : ""} \u2014 ${headline}
|
|
4854
|
+
|
|
4855
|
+
${changes.map((c) => `- ${c}`).join("\n")}`;
|
|
4856
|
+
}
|
|
4857
|
+
function renderChartBarsMd(slots, t) {
|
|
4858
|
+
const title = getSlotValue(slots, t, "title");
|
|
4859
|
+
const labels = getSlotValue(slots, t, "labels").split("\n").filter(Boolean);
|
|
4860
|
+
const values = getSlotValue(slots, t, "values").split("\n").filter(Boolean);
|
|
4861
|
+
const rows = labels.map((l, i) => `- **${l}**: ${values[i] ?? "0"}`).join("\n");
|
|
4862
|
+
return `${title ? `## ${title}
|
|
4863
|
+
|
|
4864
|
+
` : ""}${rows}`;
|
|
4865
|
+
}
|
|
4866
|
+
function renderArchitectureBadgeMd(slots, t) {
|
|
4867
|
+
const title = getSlotValue(slots, t, "title");
|
|
4868
|
+
const items = getSlotValue(slots, t, "items").split("\n").filter(Boolean);
|
|
4869
|
+
const subtitle = getSlotValue(slots, t, "subtitle");
|
|
4870
|
+
return `${title ? `**${title}**
|
|
4871
|
+
|
|
4872
|
+
` : ""}${items.map((i) => `- ${i.trim()}`).join("\n")}${subtitle ? `
|
|
4873
|
+
|
|
4874
|
+
*${subtitle}*` : ""}`;
|
|
4875
|
+
}
|
|
2253
4876
|
var MD_FN = {
|
|
2254
4877
|
"stat-card": renderStatCardMd,
|
|
2255
4878
|
"feature-grid": renderFeatureGridMd,
|
|
@@ -2258,7 +4881,11 @@ var MD_FN = {
|
|
|
2258
4881
|
"metric-badge": renderMetricBadgeMd,
|
|
2259
4882
|
"pricing-tier": renderPricingTierMd,
|
|
2260
4883
|
"changelog-row": renderChangelogRowMd,
|
|
2261
|
-
"social-proof": renderSocialProofMd
|
|
4884
|
+
"social-proof": renderSocialProofMd,
|
|
4885
|
+
"animated-stat": renderAnimatedStatMd,
|
|
4886
|
+
"release-card": renderReleaseCardMd,
|
|
4887
|
+
"chart-bars": renderChartBarsMd,
|
|
4888
|
+
"architecture-badge": renderArchitectureBadgeMd
|
|
2262
4889
|
};
|
|
2263
4890
|
function toReactStyle(vars) {
|
|
2264
4891
|
return "{\n" + Object.entries(vars).map(([k, v]) => ` "${k}": "${v.replace(/"/g, '\\"')}"`).join(",\n") + "\n }";
|
|
@@ -2290,6 +4917,89 @@ ${slotDecls}
|
|
|
2290
4917
|
}
|
|
2291
4918
|
`;
|
|
2292
4919
|
}
|
|
4920
|
+
function getRequiredLibs(templateId) {
|
|
4921
|
+
const libs = /* @__PURE__ */ new Set(["tailwind"]);
|
|
4922
|
+
if (["animated-stat", "release-card"].includes(templateId)) {
|
|
4923
|
+
libs.add("anime");
|
|
4924
|
+
}
|
|
4925
|
+
if (templateId === "chart-bars") {
|
|
4926
|
+
libs.add("anime");
|
|
4927
|
+
libs.add("chartjs");
|
|
4928
|
+
}
|
|
4929
|
+
if (templateId === "architecture-badge") {
|
|
4930
|
+
libs.add("anime");
|
|
4931
|
+
libs.add("lucide");
|
|
4932
|
+
}
|
|
4933
|
+
return Array.from(libs);
|
|
4934
|
+
}
|
|
4935
|
+
function buildCdnScripts(libs) {
|
|
4936
|
+
const tags = [];
|
|
4937
|
+
if (libs.includes("tailwind")) {
|
|
4938
|
+
tags.push(`<script>window.tailwind={config:{},silent:true}</script>`);
|
|
4939
|
+
tags.push(`<script src="https://cdn.tailwindcss.com"></script>`);
|
|
4940
|
+
}
|
|
4941
|
+
if (libs.includes("chartjs")) {
|
|
4942
|
+
tags.push(`<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.2/dist/chart.umd.min.js"></script>`);
|
|
4943
|
+
}
|
|
4944
|
+
if (libs.includes("anime")) {
|
|
4945
|
+
tags.push(`<script src="https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js"></script>`);
|
|
4946
|
+
}
|
|
4947
|
+
if (libs.includes("lucide")) {
|
|
4948
|
+
tags.push(`<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>`);
|
|
4949
|
+
}
|
|
4950
|
+
if (libs.includes("alpine")) {
|
|
4951
|
+
tags.push(`<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>`);
|
|
4952
|
+
}
|
|
4953
|
+
return tags.join("\n ");
|
|
4954
|
+
}
|
|
4955
|
+
function renderStandalone(template, slots, brand, style) {
|
|
4956
|
+
const bodyFn = HTML_BODY_FN[template.id];
|
|
4957
|
+
const body = bodyFn ? bodyFn(slots, template) : "";
|
|
4958
|
+
const allVars = { ...brandVars(brand), ...styleVars(style) };
|
|
4959
|
+
const inlineStyle = buildInlineStyle(allVars);
|
|
4960
|
+
const customCss = style.customCss ?? "";
|
|
4961
|
+
const override = style.templateOverrides?.[template.id]?.html;
|
|
4962
|
+
const finalBody = override ?? body;
|
|
4963
|
+
const libs = getRequiredLibs(template.id);
|
|
4964
|
+
const scripts = buildCdnScripts(libs);
|
|
4965
|
+
const w = template.defaultWidth ?? 600;
|
|
4966
|
+
const h = template.defaultHeight ?? 400;
|
|
4967
|
+
return `<!DOCTYPE html>
|
|
4968
|
+
<html lang="en">
|
|
4969
|
+
<head>
|
|
4970
|
+
<meta charset="UTF-8" />
|
|
4971
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
4972
|
+
<title>${escHtml(template.name)} \u2014 forge0x2B</title>
|
|
4973
|
+
${scripts}
|
|
4974
|
+
<style>
|
|
4975
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
4976
|
+
html, body {
|
|
4977
|
+
margin: 0; padding: 0;
|
|
4978
|
+
width: ${w}px; height: ${h}px;
|
|
4979
|
+
overflow: hidden;
|
|
4980
|
+
background: transparent;
|
|
4981
|
+
display: flex; align-items: center; justify-content: center;
|
|
4982
|
+
}
|
|
4983
|
+
.pw-widget {
|
|
4984
|
+
font-family: var(--pw-font-body);
|
|
4985
|
+
color: var(--pw-brand-text);
|
|
4986
|
+
background: var(--pw-brand-surface);
|
|
4987
|
+
border-radius: var(--pw-radius-md);
|
|
4988
|
+
padding: var(--pw-spacing-md);
|
|
4989
|
+
border: var(--pw-border-width) solid rgba(255,255,255,.08);
|
|
4990
|
+
box-sizing: border-box;
|
|
4991
|
+
width: 100%; max-width: ${w}px;
|
|
4992
|
+
${customCss}
|
|
4993
|
+
}
|
|
4994
|
+
</style>
|
|
4995
|
+
</head>
|
|
4996
|
+
<body>
|
|
4997
|
+
<div class="pw-widget pw-${template.id}" style="${inlineStyle}">
|
|
4998
|
+
${finalBody}
|
|
4999
|
+
</div>
|
|
5000
|
+
</body>
|
|
5001
|
+
</html>`;
|
|
5002
|
+
}
|
|
2293
5003
|
function renderWidget(input) {
|
|
2294
5004
|
const { template, slots, brandKit, format } = input;
|
|
2295
5005
|
const style = !input.style ? STYLE_PRESET_DEFAULT : typeof input.style === "string" ? getStyleById(input.style) : input.style;
|
|
@@ -2300,6 +5010,9 @@ function renderWidget(input) {
|
|
|
2300
5010
|
if (format === "react") {
|
|
2301
5011
|
return renderReact(template, slots, brandKit ?? null, style);
|
|
2302
5012
|
}
|
|
5013
|
+
if (format === "standalone") {
|
|
5014
|
+
return renderStandalone(template, slots, brandKit ?? null, style);
|
|
5015
|
+
}
|
|
2303
5016
|
const bodyFn = HTML_BODY_FN[template.id];
|
|
2304
5017
|
const body = bodyFn ? bodyFn(slots, template) : "";
|
|
2305
5018
|
const allVars = { ...brandVars(brandKit ?? null), ...styleVars(style) };
|
|
@@ -2510,71 +5223,8 @@ var FORGE_TEMPLATES = [
|
|
|
2510
5223
|
asset_slots: []
|
|
2511
5224
|
}
|
|
2512
5225
|
];
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
const resolvedTarget = path.resolve(targetPath);
|
|
2516
|
-
const resolvedFile = path.resolve(filePath);
|
|
2517
|
-
const allowedPrefix = path.join(resolvedTarget, ".prism", "blueprint");
|
|
2518
|
-
if (!resolvedFile.startsWith(allowedPrefix + "/") && resolvedFile !== allowedPrefix) {
|
|
2519
|
-
return false;
|
|
2520
|
-
}
|
|
2521
|
-
const basename = resolvedFile.slice(resolvedFile.lastIndexOf("/") + 1);
|
|
2522
|
-
return ALLOWED_BLUEPRINT_FILENAMES.has(basename);
|
|
2523
|
-
}
|
|
2524
|
-
function readBlueprintFromTarget(targetPath) {
|
|
2525
|
-
const candidates = [
|
|
2526
|
-
path.join(targetPath, ".prism", "blueprint", "snapshot.json"),
|
|
2527
|
-
path.join(targetPath, ".prism", "blueprint.json")
|
|
2528
|
-
];
|
|
2529
|
-
for (const candidate of candidates) {
|
|
2530
|
-
const normalized = path.normalize(candidate);
|
|
2531
|
-
if (!isAllowedBlueprintPath(targetPath, normalized)) continue;
|
|
2532
|
-
if (!fs$1.existsSync(normalized)) continue;
|
|
2533
|
-
try {
|
|
2534
|
-
const raw = fs$1.readFileSync(normalized, "utf-8");
|
|
2535
|
-
return JSON.parse(raw);
|
|
2536
|
-
} catch {
|
|
2537
|
-
return null;
|
|
2538
|
-
}
|
|
2539
|
-
}
|
|
2540
|
-
return null;
|
|
2541
|
-
}
|
|
2542
|
-
function zoneId(filePath) {
|
|
2543
|
-
const parts = filePath.replace(/^\//, "").split("/");
|
|
2544
|
-
return parts[0] || "root";
|
|
2545
|
-
}
|
|
2546
|
-
function extractZones(blueprint) {
|
|
2547
|
-
const zoneMap = /* @__PURE__ */ new Map();
|
|
2548
|
-
for (const file of blueprint.files) {
|
|
2549
|
-
const id = zoneId(file.path);
|
|
2550
|
-
if (!zoneMap.has(id)) zoneMap.set(id, { files: [], heat: 0 });
|
|
2551
|
-
const z = zoneMap.get(id);
|
|
2552
|
-
z.files.push(file.path);
|
|
2553
|
-
z.heat += (file.importedByCount ?? 0) + (file.importCount ?? 0);
|
|
2554
|
-
}
|
|
2555
|
-
return Array.from(zoneMap.entries()).map(([id, { files, heat }]) => ({
|
|
2556
|
-
id,
|
|
2557
|
-
name: id,
|
|
2558
|
-
files,
|
|
2559
|
-
fileCount: files.length,
|
|
2560
|
-
heat
|
|
2561
|
-
})).sort((a, b) => b.heat - a.heat || b.fileCount - a.fileCount);
|
|
2562
|
-
}
|
|
2563
|
-
function toFileEntry(f) {
|
|
2564
|
-
return {
|
|
2565
|
-
path: f.path,
|
|
2566
|
-
importCount: f.importCount ?? 0,
|
|
2567
|
-
importedByCount: f.importedByCount ?? 0,
|
|
2568
|
-
lineCount: f.lineCount ?? 0,
|
|
2569
|
-
zone: zoneId(f.path)
|
|
2570
|
-
};
|
|
2571
|
-
}
|
|
2572
|
-
function extractTopChurnFiles(blueprint, n = 10) {
|
|
2573
|
-
return blueprint.files.map(toFileEntry).sort((a, b) => b.importedByCount - a.importedByCount || b.lineCount - a.lineCount).slice(0, n);
|
|
2574
|
-
}
|
|
2575
|
-
function extractDependencyHotspots(blueprint, n = 10) {
|
|
2576
|
-
return blueprint.files.map(toFileEntry).sort((a, b) => b.importCount - a.importCount || b.importedByCount - a.importedByCount).slice(0, n);
|
|
2577
|
-
}
|
|
5226
|
+
|
|
5227
|
+
// src/forge/secrets.ts
|
|
2578
5228
|
var SECRET_PATTERNS = [
|
|
2579
5229
|
/sk-ant-api03-[A-Za-z0-9_-]{80,}/,
|
|
2580
5230
|
/sk-proj-[A-Za-z0-9_-]{40,}/,
|
|
@@ -2584,42 +5234,8 @@ var SECRET_PATTERNS = [
|
|
|
2584
5234
|
function scanForSecrets(text) {
|
|
2585
5235
|
return SECRET_PATTERNS.some((re) => re.test(text));
|
|
2586
5236
|
}
|
|
2587
|
-
var MAX_SUMMARY_CHARS = 2e3;
|
|
2588
|
-
function deriveContextSummary(blueprint) {
|
|
2589
|
-
const zones = extractZones(blueprint).slice(0, 8);
|
|
2590
|
-
const churn = extractTopChurnFiles(blueprint, 5);
|
|
2591
|
-
const deps = extractDependencyHotspots(blueprint, 5);
|
|
2592
|
-
const scanAge = blueprint.scanTimestamp ? (() => {
|
|
2593
|
-
const ageMs = Date.now() - blueprint.scanTimestamp * 1e3;
|
|
2594
|
-
const ageMin = Math.round(ageMs / 6e4);
|
|
2595
|
-
if (ageMin < 60) return `${ageMin}m ago`;
|
|
2596
|
-
if (ageMin < 1440) return `${Math.round(ageMin / 60)}h ago`;
|
|
2597
|
-
return `${Math.round(ageMin / 1440)}d ago`;
|
|
2598
|
-
})() : "unknown";
|
|
2599
|
-
const lines = [
|
|
2600
|
-
`Blueprint snapshot (scanned ${scanAge}):`,
|
|
2601
|
-
`Total files: ${blueprint.stats?.totalFiles ?? blueprint.files.length} | app:${blueprint.categories?.app ?? 0} component:${blueprint.categories?.component ?? 0} lib:${blueprint.categories?.lib ?? 0} hook:${blueprint.categories?.hook ?? 0}`,
|
|
2602
|
-
"",
|
|
2603
|
-
"Top zones (by activity):",
|
|
2604
|
-
...zones.map((z) => ` ${z.name}: ${z.fileCount} files, heat ${z.heat}`),
|
|
2605
|
-
"",
|
|
2606
|
-
"Hottest files (most imported by others):",
|
|
2607
|
-
...churn.map((f) => ` ${f.path} (importedBy:${f.importedByCount})`),
|
|
2608
|
-
"",
|
|
2609
|
-
"Most dependency-heavy files:",
|
|
2610
|
-
...deps.map((f) => ` ${f.path} (imports:${f.importCount})`)
|
|
2611
|
-
];
|
|
2612
|
-
let summary = lines.join("\n");
|
|
2613
|
-
if (summary.length > MAX_SUMMARY_CHARS) {
|
|
2614
|
-
summary = summary.slice(0, MAX_SUMMARY_CHARS - 3) + "...";
|
|
2615
|
-
}
|
|
2616
|
-
if (scanForSecrets(summary)) {
|
|
2617
|
-
throw new Error("Blueprint context summary contains unexpected secret-pattern match. Refusing to pass to LLM.");
|
|
2618
|
-
}
|
|
2619
|
-
return summary;
|
|
2620
|
-
}
|
|
2621
5237
|
|
|
2622
|
-
// src/forge/prismContext.ts
|
|
5238
|
+
// src/forge/prismContext.data.ts
|
|
2623
5239
|
var PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT = {
|
|
2624
5240
|
id: "release-announcement",
|
|
2625
5241
|
name: "Release Announcement",
|
|
@@ -2671,104 +5287,6 @@ var PRISM_TEMPLATES = [
|
|
|
2671
5287
|
function getPrismTemplate(id) {
|
|
2672
5288
|
return PRISM_TEMPLATES.find((t) => t.id === id);
|
|
2673
5289
|
}
|
|
2674
|
-
function releaseContextBlock(blueprint) {
|
|
2675
|
-
const churn = extractTopChurnFiles(blueprint, 8);
|
|
2676
|
-
const zones = extractZones(blueprint).slice(0, 6);
|
|
2677
|
-
return [
|
|
2678
|
-
"Recent activity (most active files by coupling):",
|
|
2679
|
-
...churn.map((f) => ` ${f.path} \u2014 importedBy:${f.importedByCount}`),
|
|
2680
|
-
"",
|
|
2681
|
-
"Most active zones:",
|
|
2682
|
-
...zones.map((z) => ` ${z.name}: ${z.fileCount} files, activity score ${z.heat}`)
|
|
2683
|
-
].join("\n");
|
|
2684
|
-
}
|
|
2685
|
-
function changelogContextBlock(blueprint) {
|
|
2686
|
-
const zones = extractZones(blueprint).slice(0, 10);
|
|
2687
|
-
const hotspots = extractDependencyHotspots(blueprint, 6);
|
|
2688
|
-
return [
|
|
2689
|
-
"Zone breakdown:",
|
|
2690
|
-
...zones.map((z) => ` ${z.name}: ${z.fileCount} files`),
|
|
2691
|
-
"",
|
|
2692
|
-
"High-coupling files (refactoring targets):",
|
|
2693
|
-
...hotspots.map((f) => ` ${f.path} \u2014 imports ${f.importCount} modules`)
|
|
2694
|
-
].join("\n");
|
|
2695
|
-
}
|
|
2696
|
-
function deepdiveContextBlock(blueprint) {
|
|
2697
|
-
return deriveContextSummary(blueprint);
|
|
2698
|
-
}
|
|
2699
|
-
function zoneContextBlock(blueprint, zoneName) {
|
|
2700
|
-
const allZones = extractZones(blueprint);
|
|
2701
|
-
const zone = allZones.find((z) => z.id === zoneName || z.name === zoneName);
|
|
2702
|
-
if (!zone) {
|
|
2703
|
-
return `Zone "${zoneName}" not found. Available zones: ${allZones.map((z) => z.name).join(", ")}`;
|
|
2704
|
-
}
|
|
2705
|
-
const topFiles = zone.files.slice(0, 12);
|
|
2706
|
-
const otherCount = Math.max(0, zone.fileCount - 12);
|
|
2707
|
-
return [
|
|
2708
|
-
`Zone: ${zone.name} (${zone.fileCount} files, heat score ${zone.heat})`,
|
|
2709
|
-
"Files:",
|
|
2710
|
-
...topFiles.map((f) => ` ${f}`),
|
|
2711
|
-
...otherCount > 0 ? [` ... and ${otherCount} more`] : []
|
|
2712
|
-
].join("\n");
|
|
2713
|
-
}
|
|
2714
|
-
function moduleContextBlock(blueprint, modulePath) {
|
|
2715
|
-
const file = blueprint.files.find((f) => f.path.includes(modulePath));
|
|
2716
|
-
if (!file) {
|
|
2717
|
-
return `Module path "${modulePath}" not found in blueprint.`;
|
|
2718
|
-
}
|
|
2719
|
-
const deps = extractDependencyHotspots(blueprint, 5);
|
|
2720
|
-
return [
|
|
2721
|
-
`Module: ${file.path}`,
|
|
2722
|
-
` importCount: ${file.importCount ?? 0}`,
|
|
2723
|
-
` importedByCount: ${file.importedByCount ?? 0}`,
|
|
2724
|
-
` lineCount: ${file.lineCount ?? 0}`,
|
|
2725
|
-
"",
|
|
2726
|
-
"Related high-activity files:",
|
|
2727
|
-
...deps.filter((d) => d.zone === (file.path.split("/")[0] || "root")).slice(0, 5).map((d) => ` ${d.path}`)
|
|
2728
|
-
].join("\n");
|
|
2729
|
-
}
|
|
2730
|
-
function buildPrismContextPrompt(blueprint, focus, highlight) {
|
|
2731
|
-
const resolvedFocus = focus ?? "summary";
|
|
2732
|
-
let contextBlock;
|
|
2733
|
-
let systemFragment;
|
|
2734
|
-
let suggestedAsk;
|
|
2735
|
-
if (resolvedFocus === "release") {
|
|
2736
|
-
contextBlock = releaseContextBlock(blueprint);
|
|
2737
|
-
systemFragment = "You have access to a real codebase blueprint showing which files and zones saw the most recent activity. Use this to make the release announcement concrete \u2014 reference specific zone names and file roles.";
|
|
2738
|
-
suggestedAsk = PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT.suggestedAsk;
|
|
2739
|
-
} else if (resolvedFocus === "changelog") {
|
|
2740
|
-
contextBlock = changelogContextBlock(blueprint);
|
|
2741
|
-
systemFragment = "You have access to a real codebase blueprint showing zone structure and dependency coupling. Use this to make the changelog specific and credible.";
|
|
2742
|
-
suggestedAsk = PRISM_TEMPLATE_SHIPPING_DIGEST.suggestedAsk;
|
|
2743
|
-
} else if (resolvedFocus === "deepdive") {
|
|
2744
|
-
contextBlock = deepdiveContextBlock(blueprint);
|
|
2745
|
-
systemFragment = "You have access to a complete blueprint of a real codebase. Use the zone names, file counts, and coupling metrics to write a concrete, non-generic technical explanation.";
|
|
2746
|
-
suggestedAsk = PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW.suggestedAsk;
|
|
2747
|
-
} else if (resolvedFocus.startsWith("zone:")) {
|
|
2748
|
-
const zoneName = resolvedFocus.slice(5);
|
|
2749
|
-
contextBlock = zoneContextBlock(blueprint, zoneName);
|
|
2750
|
-
systemFragment = `You have access to real file data for the "${zoneName}" zone of the codebase. Use specific file names and counts.`;
|
|
2751
|
-
suggestedAsk = PRISM_TEMPLATE_ZONE_DEEPDIVE.suggestedAsk.replace(
|
|
2752
|
-
"this codebase zone",
|
|
2753
|
-
`the "${zoneName}" zone`
|
|
2754
|
-
);
|
|
2755
|
-
} else if (resolvedFocus.startsWith("module:")) {
|
|
2756
|
-
const modulePath = resolvedFocus.slice(7);
|
|
2757
|
-
contextBlock = moduleContextBlock(blueprint, modulePath);
|
|
2758
|
-
systemFragment = `You have access to dependency data for the "${modulePath}" module. Reference specific metrics.`;
|
|
2759
|
-
suggestedAsk = `Explain the role of ${modulePath} in the codebase architecture and why it matters.`;
|
|
2760
|
-
} else {
|
|
2761
|
-
contextBlock = deriveContextSummary(blueprint);
|
|
2762
|
-
systemFragment = "You have access to a real codebase blueprint. Use specific zone names and file counts to make your content concrete and credible.";
|
|
2763
|
-
suggestedAsk = "Summarize the structure and key areas of this codebase.";
|
|
2764
|
-
}
|
|
2765
|
-
if (highlight) {
|
|
2766
|
-
contextBlock += `
|
|
2767
|
-
|
|
2768
|
-
User wants to highlight: ${highlight}`;
|
|
2769
|
-
}
|
|
2770
|
-
return { systemFragment, contextBlock, suggestedAsk };
|
|
2771
|
-
}
|
|
2772
5290
|
|
|
2773
5291
|
// src/forge/channel.ts
|
|
2774
5292
|
var DISPATCH_CHANNEL_TWEET = {
|
|
@@ -2850,7 +5368,7 @@ function getDispatchChannel(id) {
|
|
|
2850
5368
|
}
|
|
2851
5369
|
|
|
2852
5370
|
// src/forge/dispatch.ts
|
|
2853
|
-
function
|
|
5371
|
+
function buildSystemPrompt14(channel, audience, brand, blueprintContext, toneOffset) {
|
|
2854
5372
|
const lines = [
|
|
2855
5373
|
`You are a skilled content writer producing a ${channel.name} post.`,
|
|
2856
5374
|
"",
|
|
@@ -2901,7 +5419,7 @@ function truncate(content, maxLength) {
|
|
|
2901
5419
|
async function generateForChannel(ask, channel, provider, opts) {
|
|
2902
5420
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2903
5421
|
try {
|
|
2904
|
-
const system =
|
|
5422
|
+
const system = buildSystemPrompt14(
|
|
2905
5423
|
channel,
|
|
2906
5424
|
opts.audience,
|
|
2907
5425
|
opts.brand,
|
|
@@ -3046,7 +5564,7 @@ function nextVersionNumber(versions) {
|
|
|
3046
5564
|
}
|
|
3047
5565
|
|
|
3048
5566
|
// src/forge/refineAsset.ts
|
|
3049
|
-
function
|
|
5567
|
+
function buildSystemPrompt15(input) {
|
|
3050
5568
|
const parts = [
|
|
3051
5569
|
`You are a content refinement assistant for a developer-focused content tool (forge0x2B).`,
|
|
3052
5570
|
`You are refining an existing ${input.assetType.replace(/-/g, " ")} asset.`,
|
|
@@ -3085,7 +5603,7 @@ function parseRefinedResponse(raw) {
|
|
|
3085
5603
|
return { newContent, llmReply };
|
|
3086
5604
|
}
|
|
3087
5605
|
async function refineAsset(input, provider) {
|
|
3088
|
-
const systemPrompt =
|
|
5606
|
+
const systemPrompt = buildSystemPrompt15(input);
|
|
3089
5607
|
if (scanForSecrets(systemPrompt)) {
|
|
3090
5608
|
throw new Error("refineAsset: secret pattern detected in asset content. Refusing to send to LLM.");
|
|
3091
5609
|
}
|
|
@@ -3299,7 +5817,6 @@ exports.asAudienceId = asAudienceId;
|
|
|
3299
5817
|
exports.assembleBrandUrlExtractionPrompt = assembleBrandUrlExtractionPrompt;
|
|
3300
5818
|
exports.assembleForgePrompt = assembleForgePrompt;
|
|
3301
5819
|
exports.brandThemeConfigToEntry = brandThemeConfigToEntry;
|
|
3302
|
-
exports.buildPrismContextPrompt = buildPrismContextPrompt;
|
|
3303
5820
|
exports.buildRevertVersion = buildRevertVersion;
|
|
3304
5821
|
exports.buildScheduledEntry = buildScheduledEntry;
|
|
3305
5822
|
exports.buildVersion = buildVersion;
|
|
@@ -3308,22 +5825,25 @@ exports.clampAnimationDuration = clampAnimationDuration;
|
|
|
3308
5825
|
exports.computeDiff = computeDiff;
|
|
3309
5826
|
exports.defaultBrandKit = defaultBrandKit;
|
|
3310
5827
|
exports.defaultValueForField = defaultValueForField;
|
|
3311
|
-
exports.deriveContextSummary = deriveContextSummary;
|
|
3312
5828
|
exports.distill = distill;
|
|
3313
5829
|
exports.entryInRange = entryInRange;
|
|
3314
5830
|
exports.exportToBufferCsv = exportToBufferCsv;
|
|
3315
5831
|
exports.exportToHypefuryCsv = exportToHypefuryCsv;
|
|
3316
5832
|
exports.exportToICalendar = exportToICalendar;
|
|
3317
|
-
exports.
|
|
3318
|
-
exports.
|
|
3319
|
-
exports.extractTopChurnFiles = extractTopChurnFiles;
|
|
3320
|
-
exports.extractZones = extractZones;
|
|
5833
|
+
exports.generateADR = generateADR;
|
|
5834
|
+
exports.generateArc42 = generateArc42;
|
|
3321
5835
|
exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
|
|
3322
5836
|
exports.generateAskDrivenAsset = generateAskDrivenAsset;
|
|
3323
5837
|
exports.generateChangesSince = generateChangesSince;
|
|
5838
|
+
exports.generateComplianceDoc = generateComplianceDoc;
|
|
5839
|
+
exports.generateKnowledgeCapture = generateKnowledgeCapture;
|
|
5840
|
+
exports.generateNewsletter = generateNewsletter;
|
|
3324
5841
|
exports.generateOnboardingDoc = generateOnboardingDoc;
|
|
5842
|
+
exports.generatePresentation = generatePresentation;
|
|
5843
|
+
exports.generateRadio = generateRadio;
|
|
3325
5844
|
exports.generateRefactoringReport = generateRefactoringReport;
|
|
3326
5845
|
exports.generateReleaseNotes = generateReleaseNotes;
|
|
5846
|
+
exports.generateSprintRetro = generateSprintRetro;
|
|
3327
5847
|
exports.getDispatchChannel = getDispatchChannel;
|
|
3328
5848
|
exports.getPrismTemplate = getPrismTemplate;
|
|
3329
5849
|
exports.getSlotValue = getSlotValue;
|
|
@@ -3341,9 +5861,6 @@ exports.parseStyleFromTailwindConfig = parseStyleFromTailwindConfig;
|
|
|
3341
5861
|
exports.parseStyleFromTokensJson = parseStyleFromTokensJson;
|
|
3342
5862
|
exports.parseThemeConfigContent = parseThemeConfigContent;
|
|
3343
5863
|
exports.previewExport = previewExport;
|
|
3344
|
-
exports.readBlueprintData = readBlueprintData;
|
|
3345
|
-
exports.readBlueprintFromTarget = readBlueprintFromTarget;
|
|
3346
|
-
exports.readPrismDirectory = readPrismDirectory;
|
|
3347
5864
|
exports.refineAsset = refineAsset;
|
|
3348
5865
|
exports.refineLimitState = refineLimitState;
|
|
3349
5866
|
exports.renderWidget = renderWidget;
|