forgesmith 0.3.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3373 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs/promises');
4
+ var path = require('path');
5
+ var fs$1 = require('fs');
6
+ var crypto = require('crypto');
7
+
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
+ // src/prism/reader.ts
14
+ async function readJsonFiles(dir) {
15
+ try {
16
+ const entries = await fs__default.default.readdir(dir);
17
+ const results = [];
18
+ for (const entry of entries) {
19
+ if (!entry.endsWith(".json")) continue;
20
+ try {
21
+ const raw = await fs__default.default.readFile(path__default.default.join(dir, entry), "utf-8");
22
+ results.push(JSON.parse(raw));
23
+ } catch {
24
+ }
25
+ }
26
+ return results;
27
+ } catch {
28
+ return [];
29
+ }
30
+ }
31
+ async function readPrismDirectory(prismPath) {
32
+ const sessionsDir = path__default.default.join(prismPath, "sessions");
33
+ const recsDir = path__default.default.join(prismPath, "recommendations");
34
+ const insightsDir = path__default.default.join(prismPath, "green", "insights", "accepted");
35
+ const [sessions, recommendations, insights] = await Promise.all([
36
+ readJsonFiles(sessionsDir),
37
+ readJsonFiles(recsDir),
38
+ readJsonFiles(insightsDir)
39
+ ]);
40
+ return { sessions, recommendations, insights };
41
+ }
42
+ async function readBlueprintData(targetPath) {
43
+ const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
44
+ try {
45
+ const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
46
+ return JSON.parse(raw);
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ // src/forge/secrets.ts
53
+ var SECRET_PATTERNS = [
54
+ /sk-ant-api03-[A-Za-z0-9_-]{80,}/,
55
+ /sk-proj-[A-Za-z0-9_-]{40,}/,
56
+ /AKIA[0-9A-Z]{16}/,
57
+ /(?:password|secret|token|api_key)\s*[:=]\s*["']?[A-Za-z0-9_\-+/]{16,}/i
58
+ ];
59
+ function scanForSecrets(text) {
60
+ return SECRET_PATTERNS.some((re) => re.test(text));
61
+ }
62
+
63
+ // src/forge/blueprintReader.ts
64
+ var ALLOWED_BLUEPRINT_FILENAMES = /* @__PURE__ */ new Set(["blueprint.json", "snapshot.json"]);
65
+ function isAllowedBlueprintPath(targetPath, filePath) {
66
+ const resolvedTarget = path.resolve(targetPath);
67
+ const resolvedFile = path.resolve(filePath);
68
+ const allowedPrefix = path.join(resolvedTarget, ".prism", "blueprint");
69
+ if (!resolvedFile.startsWith(allowedPrefix + "/") && resolvedFile !== allowedPrefix) {
70
+ return false;
71
+ }
72
+ const basename = resolvedFile.slice(resolvedFile.lastIndexOf("/") + 1);
73
+ return ALLOWED_BLUEPRINT_FILENAMES.has(basename);
74
+ }
75
+ function readBlueprintFromTarget(targetPath) {
76
+ const candidates = [
77
+ path.join(targetPath, ".prism", "blueprint", "snapshot.json"),
78
+ path.join(targetPath, ".prism", "blueprint.json")
79
+ ];
80
+ for (const candidate of candidates) {
81
+ const normalized = path.normalize(candidate);
82
+ if (!isAllowedBlueprintPath(targetPath, normalized)) continue;
83
+ if (!fs$1.existsSync(normalized)) continue;
84
+ try {
85
+ const raw = fs$1.readFileSync(normalized, "utf-8");
86
+ return JSON.parse(raw);
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function zoneId(filePath) {
94
+ const parts = filePath.replace(/^\//, "").split("/");
95
+ return parts[0] || "root";
96
+ }
97
+ function extractZones(blueprint) {
98
+ const zoneMap = /* @__PURE__ */ new Map();
99
+ for (const file of blueprint.files) {
100
+ const id = zoneId(file.path);
101
+ if (!zoneMap.has(id)) zoneMap.set(id, { files: [], heat: 0 });
102
+ const z = zoneMap.get(id);
103
+ z.files.push(file.path);
104
+ z.heat += (file.importedByCount ?? 0) + (file.importCount ?? 0);
105
+ }
106
+ return Array.from(zoneMap.entries()).map(([id, { files, heat }]) => ({
107
+ id,
108
+ name: id,
109
+ files,
110
+ fileCount: files.length,
111
+ heat
112
+ })).sort((a, b) => b.heat - a.heat || b.fileCount - a.fileCount);
113
+ }
114
+ function toFileEntry(f) {
115
+ return {
116
+ path: f.path,
117
+ importCount: f.importCount ?? 0,
118
+ importedByCount: f.importedByCount ?? 0,
119
+ lineCount: f.lineCount ?? 0,
120
+ zone: zoneId(f.path)
121
+ };
122
+ }
123
+ function extractTopChurnFiles(blueprint, n = 10) {
124
+ return blueprint.files.map(toFileEntry).sort((a, b) => b.importedByCount - a.importedByCount || b.lineCount - a.lineCount).slice(0, n);
125
+ }
126
+ function extractDependencyHotspots(blueprint, n = 10) {
127
+ return blueprint.files.map(toFileEntry).sort((a, b) => b.importCount - a.importCount || b.importedByCount - a.importedByCount).slice(0, n);
128
+ }
129
+ var MAX_SUMMARY_CHARS = 2e3;
130
+ function deriveContextSummary(blueprint) {
131
+ const zones = extractZones(blueprint).slice(0, 8);
132
+ const churn = extractTopChurnFiles(blueprint, 5);
133
+ const deps = extractDependencyHotspots(blueprint, 5);
134
+ const scanAge = blueprint.scanTimestamp ? (() => {
135
+ const ageMs = Date.now() - blueprint.scanTimestamp * 1e3;
136
+ const ageMin = Math.round(ageMs / 6e4);
137
+ if (ageMin < 60) return `${ageMin}m ago`;
138
+ if (ageMin < 1440) return `${Math.round(ageMin / 60)}h ago`;
139
+ return `${Math.round(ageMin / 1440)}d ago`;
140
+ })() : "unknown";
141
+ const lines = [
142
+ `Blueprint snapshot (scanned ${scanAge}):`,
143
+ `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}`,
144
+ "",
145
+ "Top zones (by activity):",
146
+ ...zones.map((z) => ` ${z.name}: ${z.fileCount} files, heat ${z.heat}`),
147
+ "",
148
+ "Hottest files (most imported by others):",
149
+ ...churn.map((f) => ` ${f.path} (importedBy:${f.importedByCount})`),
150
+ "",
151
+ "Most dependency-heavy files:",
152
+ ...deps.map((f) => ` ${f.path} (imports:${f.importCount})`)
153
+ ];
154
+ let summary = lines.join("\n");
155
+ if (summary.length > MAX_SUMMARY_CHARS) {
156
+ summary = summary.slice(0, MAX_SUMMARY_CHARS - 3) + "...";
157
+ }
158
+ if (scanForSecrets(summary)) {
159
+ throw new Error("Blueprint context summary contains unexpected secret-pattern match. Refusing to pass to LLM.");
160
+ }
161
+ return summary;
162
+ }
163
+ function deriveTechnicalScore(categories, total) {
164
+ if (total === 0) return 50;
165
+ const techFiles = (categories.lib ?? 0) + (categories.hook ?? 0);
166
+ const appFiles = (categories.app ?? 0) + (categories.component ?? 0);
167
+ const techRatio = techFiles / total;
168
+ const appRatio = appFiles / total;
169
+ return Math.round(30 + techRatio * 50 - appRatio * 10);
170
+ }
171
+ function deriveAudienceMix(categories, paths) {
172
+ const hasAdminPaths = paths.some((p) => /admin|dashboard|ops/.test(p));
173
+ const hasApiPaths = paths.some((p) => /\/api\/|route\.ts/.test(p));
174
+ const hasFrontend = (categories.component ?? 0) > 5;
175
+ if (hasAdminPaths && hasApiPaths) return "developer+operator";
176
+ if (hasApiPaths && !hasFrontend) return "developer";
177
+ if (hasFrontend && !hasApiPaths) return "end-user";
178
+ return "developer+business";
179
+ }
180
+ async function extractBrandFromPrismBlueprint(targetPath) {
181
+ const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
182
+ let blueprint = {};
183
+ try {
184
+ const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
185
+ blueprint = JSON.parse(raw);
186
+ } catch {
187
+ }
188
+ const categories = blueprint.categories ?? {};
189
+ const files = blueprint.files ?? [];
190
+ const paths = files.map((f) => f.path ?? "");
191
+ const total = blueprint.stats?.totalFiles ?? files.length;
192
+ const technicalScore = deriveTechnicalScore(categories, total);
193
+ const audienceMix = deriveAudienceMix(categories, paths);
194
+ const projectName = blueprint.targetPath ? path__default.default.basename(blueprint.targetPath) : "Project";
195
+ const formality = technicalScore > 60 ? 65 : 50;
196
+ const technicality = Math.min(95, technicalScore + 10);
197
+ return {
198
+ suggestedName: projectName,
199
+ voice: {
200
+ tone: technicalScore > 60 ? "technical" : "professional",
201
+ audience: audienceMix.includes("developer") ? "developers" : "business",
202
+ vocabulary: [],
203
+ avoid: [],
204
+ formality,
205
+ technicality
206
+ },
207
+ prism_detected: true,
208
+ blueprint_path: snapshotPath,
209
+ stats: {
210
+ totalFiles: total,
211
+ categories,
212
+ technicalScore,
213
+ audienceMix
214
+ }
215
+ };
216
+ }
217
+
218
+ // src/forge/prismContext.data.ts
219
+ var PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT = {
220
+ id: "release-announcement",
221
+ name: "Release Announcement",
222
+ description: "Announce what changed based on recent blueprint activity.",
223
+ defaultFocus: "release",
224
+ suggestedAsk: "Write a release announcement for the recent changes in our codebase. Reference the specific zones and files that saw the most activity. Make it concrete \u2014 mention zone names and what they do.",
225
+ suggestedChannels: ["tweet", "linkedin", "newsletter"]
226
+ };
227
+ var PRISM_TEMPLATE_SHIPPING_DIGEST = {
228
+ id: "weekly-shipping-digest",
229
+ name: "Weekly Shipping Digest",
230
+ description: "What we shipped this week \u2014 grounded in real zone activity.",
231
+ defaultFocus: "changelog",
232
+ suggestedAsk: "Write a 'what we shipped this week' digest. Use the zone and file activity data to describe what changed. Highlight the most active areas and what that suggests about team focus.",
233
+ suggestedChannels: ["newsletter", "slack", "linkedin"]
234
+ };
235
+ var PRISM_TEMPLATE_ZONE_DEEPDIVE = {
236
+ id: "zone-deepdive",
237
+ name: "Zone Deep-Dive",
238
+ description: "Deep-dive explanation of a specific zone for developers.",
239
+ defaultFocus: "deepdive",
240
+ suggestedAsk: "Write a deep-dive technical explanation of this codebase zone. Describe what lives here, how it connects to the rest of the system, and why it matters. Be concrete \u2014 mention specific files and their roles.",
241
+ suggestedChannels: ["blog", "hn"],
242
+ requiresZone: true
243
+ };
244
+ var PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW = {
245
+ id: "architecture-overview",
246
+ name: "Architecture Overview",
247
+ description: "Full codebase structure \u2014 onboarding or blog post.",
248
+ defaultFocus: "deepdive",
249
+ suggestedAsk: "Write an architecture overview of this codebase for a new engineer joining the team. Explain the major zones, how they connect, and which files are most central. Be specific \u2014 mention real zone names from the blueprint.",
250
+ suggestedChannels: ["blog", "newsletter"]
251
+ };
252
+ var PRISM_TEMPLATE_REFACTOR_RATIONALE = {
253
+ id: "refactor-rationale",
254
+ name: "Refactor Rationale",
255
+ description: "Explain why changes were made \u2014 dependency + coupling context.",
256
+ defaultFocus: "release",
257
+ suggestedAsk: "Write a technical rationale explaining recent structural changes to the codebase. Focus on the dependency hotspots and what reducing coupling in those areas achieves. Use real file names and zone names from the blueprint.",
258
+ suggestedChannels: ["blog", "email", "hn"]
259
+ };
260
+ var PRISM_TEMPLATES = [
261
+ PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT,
262
+ PRISM_TEMPLATE_SHIPPING_DIGEST,
263
+ PRISM_TEMPLATE_ZONE_DEEPDIVE,
264
+ PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW,
265
+ PRISM_TEMPLATE_REFACTOR_RATIONALE
266
+ ];
267
+ function getPrismTemplate(id) {
268
+ return PRISM_TEMPLATES.find((t) => t.id === id);
269
+ }
270
+
271
+ // src/forge/prismContext.ts
272
+ function releaseContextBlock(blueprint) {
273
+ const churn = extractTopChurnFiles(blueprint, 8);
274
+ const zones = extractZones(blueprint).slice(0, 6);
275
+ return [
276
+ "Recent activity (most active files by coupling):",
277
+ ...churn.map((f) => ` ${f.path} \u2014 importedBy:${f.importedByCount}`),
278
+ "",
279
+ "Most active zones:",
280
+ ...zones.map((z) => ` ${z.name}: ${z.fileCount} files, activity score ${z.heat}`)
281
+ ].join("\n");
282
+ }
283
+ function changelogContextBlock(blueprint) {
284
+ const zones = extractZones(blueprint).slice(0, 10);
285
+ const hotspots = extractDependencyHotspots(blueprint, 6);
286
+ return [
287
+ "Zone breakdown:",
288
+ ...zones.map((z) => ` ${z.name}: ${z.fileCount} files`),
289
+ "",
290
+ "High-coupling files (refactoring targets):",
291
+ ...hotspots.map((f) => ` ${f.path} \u2014 imports ${f.importCount} modules`)
292
+ ].join("\n");
293
+ }
294
+ function deepdiveContextBlock(blueprint) {
295
+ return deriveContextSummary(blueprint);
296
+ }
297
+ function zoneContextBlock(blueprint, zoneName) {
298
+ const allZones = extractZones(blueprint);
299
+ const zone = allZones.find((z) => z.id === zoneName || z.name === zoneName);
300
+ if (!zone) {
301
+ return `Zone "${zoneName}" not found. Available zones: ${allZones.map((z) => z.name).join(", ")}`;
302
+ }
303
+ const topFiles = zone.files.slice(0, 12);
304
+ const otherCount = Math.max(0, zone.fileCount - 12);
305
+ return [
306
+ `Zone: ${zone.name} (${zone.fileCount} files, heat score ${zone.heat})`,
307
+ "Files:",
308
+ ...topFiles.map((f) => ` ${f}`),
309
+ ...otherCount > 0 ? [` ... and ${otherCount} more`] : []
310
+ ].join("\n");
311
+ }
312
+ function moduleContextBlock(blueprint, modulePath) {
313
+ const file = blueprint.files.find((f) => f.path.includes(modulePath));
314
+ if (!file) {
315
+ return `Module path "${modulePath}" not found in blueprint.`;
316
+ }
317
+ const deps = extractDependencyHotspots(blueprint, 5);
318
+ return [
319
+ `Module: ${file.path}`,
320
+ ` importCount: ${file.importCount ?? 0}`,
321
+ ` importedByCount: ${file.importedByCount ?? 0}`,
322
+ ` lineCount: ${file.lineCount ?? 0}`,
323
+ "",
324
+ "Related high-activity files:",
325
+ ...deps.filter((d) => d.zone === (file.path.split("/")[0] || "root")).slice(0, 5).map((d) => ` ${d.path}`)
326
+ ].join("\n");
327
+ }
328
+ function buildPrismContextPrompt(blueprint, focus, highlight) {
329
+ const resolvedFocus = focus ?? "summary";
330
+ let contextBlock;
331
+ let systemFragment;
332
+ let suggestedAsk;
333
+ if (resolvedFocus === "release") {
334
+ contextBlock = releaseContextBlock(blueprint);
335
+ 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.";
336
+ suggestedAsk = PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT.suggestedAsk;
337
+ } else if (resolvedFocus === "changelog") {
338
+ contextBlock = changelogContextBlock(blueprint);
339
+ systemFragment = "You have access to a real codebase blueprint showing zone structure and dependency coupling. Use this to make the changelog specific and credible.";
340
+ suggestedAsk = PRISM_TEMPLATE_SHIPPING_DIGEST.suggestedAsk;
341
+ } else if (resolvedFocus === "deepdive") {
342
+ contextBlock = deepdiveContextBlock(blueprint);
343
+ 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.";
344
+ suggestedAsk = PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW.suggestedAsk;
345
+ } else if (resolvedFocus.startsWith("zone:")) {
346
+ const zoneName = resolvedFocus.slice(5);
347
+ contextBlock = zoneContextBlock(blueprint, zoneName);
348
+ systemFragment = `You have access to real file data for the "${zoneName}" zone of the codebase. Use specific file names and counts.`;
349
+ suggestedAsk = PRISM_TEMPLATE_ZONE_DEEPDIVE.suggestedAsk.replace(
350
+ "this codebase zone",
351
+ `the "${zoneName}" zone`
352
+ );
353
+ } else if (resolvedFocus.startsWith("module:")) {
354
+ const modulePath = resolvedFocus.slice(7);
355
+ contextBlock = moduleContextBlock(blueprint, modulePath);
356
+ systemFragment = `You have access to dependency data for the "${modulePath}" module. Reference specific metrics.`;
357
+ suggestedAsk = `Explain the role of ${modulePath} in the codebase architecture and why it matters.`;
358
+ } else {
359
+ contextBlock = deriveContextSummary(blueprint);
360
+ systemFragment = "You have access to a real codebase blueprint. Use specific zone names and file counts to make your content concrete and credible.";
361
+ suggestedAsk = "Summarize the structure and key areas of this codebase.";
362
+ }
363
+ if (highlight) {
364
+ contextBlock += `
365
+
366
+ User wants to highlight: ${highlight}`;
367
+ }
368
+ return { systemFragment, contextBlock, suggestedAsk };
369
+ }
370
+
371
+ // src/generators/releaseNotes.ts
372
+ function buildSystemPrompt() {
373
+ return `You are a technical writer and developer-relations expert. You generate clear, accurate release notes from structured code-intelligence data. Write only the release notes \u2014 no preamble, no meta-commentary.`;
374
+ }
375
+ function buildUserPrompt(data, opts) {
376
+ const tone = opts.tone ?? "professional";
377
+ const length = opts.length ?? "medium";
378
+ const format = opts.format ?? "markdown";
379
+ const lengthGuide = { short: "2-3 paragraphs or bullet groups", medium: "4-6 sections", long: "comprehensive, 6+ sections with details" }[length];
380
+ const toneGuide = { professional: "formal, clear, business-appropriate", casual: "friendly, approachable, conversational", technical: "precise, implementation-focused, developer-centric" }[tone];
381
+ const hasSessions = (data.sessions?.length ?? 0) > 0;
382
+ const hasRecs = (data.recommendations?.length ?? 0) > 0;
383
+ const hasInsights = (data.insights?.length ?? 0) > 0;
384
+ if (!hasSessions && !hasRecs && !hasInsights) {
385
+ return `Generate a brief release note in ${format} format stating there are no changes to report in this period${data.fromDate ? ` (${data.fromDate} to ${data.toDate ?? "now"})` : ""}.`;
386
+ }
387
+ const lines = [];
388
+ lines.push(`Generate release notes with the following requirements:`);
389
+ lines.push(`- Tone: ${tone} (${toneGuide})`);
390
+ lines.push(`- Length: ${length} (${lengthGuide})`);
391
+ lines.push(`- Format: ${format}`);
392
+ if (data.fromDate) lines.push(`- Period: ${data.fromDate}${data.toDate ? ` to ${data.toDate}` : " to now"}`);
393
+ lines.push(``);
394
+ if (hasSessions) {
395
+ lines.push(`## Sessions (${data.sessions.length})`);
396
+ for (const s of data.sessions) {
397
+ lines.push(`- **${s.title}**${s.summary ? `: ${s.summary}` : ""}${s.conclusion ? ` | Conclusion: ${s.conclusion}` : ""}`);
398
+ }
399
+ lines.push(``);
400
+ }
401
+ if (hasRecs) {
402
+ lines.push(`## Recommendations (${data.recommendations.length})`);
403
+ for (const r of data.recommendations) {
404
+ const accepted = r.accepted === true ? " [ACCEPTED]" : r.accepted === false ? " [DECLINED]" : "";
405
+ lines.push(`- [${r.severity?.toUpperCase() ?? "INFO"}${accepted}] **${r.title}**${r.description ? `: ${r.description}` : ""}`);
406
+ }
407
+ lines.push(``);
408
+ }
409
+ if (hasInsights) {
410
+ lines.push(`## Insights (${data.insights.length})`);
411
+ for (const i of data.insights) {
412
+ lines.push(`- **${i.title}**${i.body ? `: ${i.body}` : ""}`);
413
+ }
414
+ lines.push(``);
415
+ }
416
+ return lines.join("\n");
417
+ }
418
+ async function generateReleaseNotes(data, opts, provider) {
419
+ const systemPrompt = buildSystemPrompt();
420
+ const userPrompt = buildUserPrompt(data, opts);
421
+ const response = await provider.complete({
422
+ systemPrompt,
423
+ messages: [{ role: "user", content: userPrompt }],
424
+ maxTokens: 2048
425
+ });
426
+ return {
427
+ text: response.content,
428
+ metadata: {
429
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
430
+ usedTokens: response.usedTokens,
431
+ generator: "forgesmith"
432
+ }
433
+ };
434
+ }
435
+
436
+ // src/generators/architectureWalkthrough.ts
437
+ var NO_DATA_MSG = "No Blueprint data available. Run prism scan first.";
438
+ function buildSystemPrompt2(tone) {
439
+ const toneMap = {
440
+ technical: "You are a senior software architect who writes precise, technical documentation for engineering teams.",
441
+ casual: "You are a friendly developer advocate who explains codebases in an approachable, conversational style.",
442
+ onboarding: "You are an experienced engineering mentor who helps new developers quickly understand a codebase."
443
+ };
444
+ return `${toneMap[tone] ?? toneMap.technical} Write only the requested document \u2014 no preamble, no meta-commentary.`;
445
+ }
446
+ function buildUserPrompt2(blueprint, opts) {
447
+ const tone = opts.tone ?? "technical";
448
+ const length = opts.length ?? "long";
449
+ const format = opts.format ?? "markdown";
450
+ const lengthGuide = {
451
+ short: "3-4 sections, concise overview",
452
+ medium: "5-7 sections with moderate detail",
453
+ long: "comprehensive, 8+ sections with full detail and examples"
454
+ }[length];
455
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 15);
456
+ const edgeSample = blueprint.edges.slice(0, 20);
457
+ const lines = [
458
+ `Generate an architecture walkthrough document in ${format} format for the codebase at "${blueprint.targetPath}".`,
459
+ `Tone: ${tone}. Length: ${length} (${lengthGuide}).`,
460
+ ``,
461
+ `## Codebase Stats`,
462
+ `- Total files: ${blueprint.stats.totalFiles}`,
463
+ `- Runtime dependency edges: ${blueprint.stats.runtimeEdges}`,
464
+ `- Categories: ${JSON.stringify(blueprint.categories)}`,
465
+ ``,
466
+ `## Key Files (by incoming dependency count)`,
467
+ ...topFiles.map((f) => `- ${f.path} [${f.category ?? "unknown"}] \u2014 importedBy: ${f.importedByCount ?? 0}, imports: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
468
+ ``,
469
+ `## Dependency Edge Sample`,
470
+ ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}${e.type ? ` (${e.type})` : ""}`),
471
+ ``,
472
+ `Cover: architecture overview, key layers/folders, entry points, important relationships, and what a new developer should understand first.`
473
+ ];
474
+ return lines.join("\n");
475
+ }
476
+ async function generateArchitectureWalkthrough(blueprint, opts, provider) {
477
+ if (!blueprint) {
478
+ return {
479
+ text: NO_DATA_MSG,
480
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
481
+ };
482
+ }
483
+ const response = await provider.complete({
484
+ systemPrompt: buildSystemPrompt2(opts.tone ?? "technical"),
485
+ messages: [{ role: "user", content: buildUserPrompt2(blueprint, opts) }],
486
+ maxTokens: 4096
487
+ });
488
+ return {
489
+ text: response.content,
490
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
491
+ };
492
+ }
493
+
494
+ // src/generators/changesSince.ts
495
+ var NO_DATA_MSG2 = "No Blueprint data available. Run prism scan first.";
496
+ function buildSystemPrompt3() {
497
+ return `You are a technical writer who produces clear, concise architecture change summaries for development teams. Focus on what changed and why it matters. Write only the document \u2014 no preamble, no meta-commentary.`;
498
+ }
499
+ function computeDelta(current, previous) {
500
+ const currentPaths = new Set(current.files.map((f) => f.path));
501
+ const previousPaths = new Set(previous.files.map((f) => f.path));
502
+ const added = current.files.filter((f) => !previousPaths.has(f.path));
503
+ const removed = previous.files.filter((f) => !currentPaths.has(f.path));
504
+ const currentEdgeKeys = new Set(current.edges.map((e) => `${e.from}\u2192${e.to}`));
505
+ const previousEdgeKeys = new Set(previous.edges.map((e) => `${e.from}\u2192${e.to}`));
506
+ const addedEdges = current.edges.filter((e) => !previousEdgeKeys.has(`${e.from}\u2192${e.to}`));
507
+ const removedEdges = previous.edges.filter((e) => !currentEdgeKeys.has(`${e.from}\u2192${e.to}`));
508
+ return { added, removed, addedEdges, removedEdges };
509
+ }
510
+ function buildUserPrompt3(current, previous, opts) {
511
+ const tone = opts.tone ?? "professional";
512
+ const length = opts.length ?? "medium";
513
+ const format = opts.format ?? "markdown";
514
+ const delta = computeDelta(current, previous);
515
+ const prevDate = new Date(previous.scanTimestamp).toISOString().split("T")[0];
516
+ const currDate = new Date(current.scanTimestamp).toISOString().split("T")[0];
517
+ const lines = [
518
+ `Generate an architecture change summary in ${format} format.`,
519
+ `Tone: ${tone}. Length: ${length}.`,
520
+ `Period: ${prevDate} \u2192 ${currDate}`,
521
+ ``,
522
+ `## File Changes`,
523
+ `- Added files (${delta.added.length}): ${delta.added.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
524
+ `- Removed files (${delta.removed.length}): ${delta.removed.slice(0, 20).map((f) => f.path).join(", ") || "none"}`,
525
+ ``,
526
+ `## Dependency Edge Changes`,
527
+ `- New edges (${delta.addedEdges.length}): ${delta.addedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
528
+ `- Removed edges (${delta.removedEdges.length}): ${delta.removedEdges.slice(0, 10).map((e) => `${e.from}\u2192${e.to}`).join(", ") || "none"}`,
529
+ ``,
530
+ `## Stats Delta`,
531
+ `- Files: ${previous.stats.totalFiles} \u2192 ${current.stats.totalFiles} (${current.stats.totalFiles - previous.stats.totalFiles >= 0 ? "+" : ""}${current.stats.totalFiles - previous.stats.totalFiles})`,
532
+ `- Edges: ${previous.stats.runtimeEdges} \u2192 ${current.stats.runtimeEdges} (${current.stats.runtimeEdges - previous.stats.runtimeEdges >= 0 ? "+" : ""}${current.stats.runtimeEdges - previous.stats.runtimeEdges})`,
533
+ ``,
534
+ `Summarize what changed architecturally, highlight significant additions or removals, and note any structural shifts.`
535
+ ];
536
+ return lines.join("\n");
537
+ }
538
+ async function generateChangesSince(current, previous, opts, provider) {
539
+ if (!current || !previous) {
540
+ return {
541
+ text: NO_DATA_MSG2,
542
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
543
+ };
544
+ }
545
+ const response = await provider.complete({
546
+ systemPrompt: buildSystemPrompt3(),
547
+ messages: [{ role: "user", content: buildUserPrompt3(current, previous, opts) }],
548
+ maxTokens: 2048
549
+ });
550
+ return {
551
+ text: response.content,
552
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
553
+ };
554
+ }
555
+
556
+ // src/generators/onboardingDoc.ts
557
+ var NO_DATA_MSG3 = "No Blueprint data available. Run prism scan first.";
558
+ function buildSystemPrompt4(tone) {
559
+ const toneMap = {
560
+ friendly: "You are a helpful senior developer writing a welcoming onboarding guide for new team members. Be warm, encouraging, and practical.",
561
+ formal: "You are a documentation engineer writing a structured onboarding reference for enterprise engineering teams.",
562
+ technical: "You are a senior engineer writing a technical onboarding document focused on implementation details and system internals."
563
+ };
564
+ return `${toneMap[tone] ?? toneMap.friendly} Write only the document \u2014 no preamble, no meta-commentary.`;
565
+ }
566
+ function buildUserPrompt4(blueprint, opts) {
567
+ const tone = opts.tone ?? "friendly";
568
+ const length = opts.length ?? "medium";
569
+ const format = opts.format ?? "markdown";
570
+ const topEntryPoints = blueprint.files.filter((f) => f.category === "app" || (f.importedByCount ?? 0) === 0).slice(0, 10);
571
+ const topImported = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 10);
572
+ const folderMap = /* @__PURE__ */ new Map();
573
+ for (const f of blueprint.files) {
574
+ const parts = f.path.split("/");
575
+ if (parts.length > 1) {
576
+ const folder = parts[0];
577
+ folderMap.set(folder, (folderMap.get(folder) ?? 0) + 1);
578
+ }
579
+ }
580
+ const topFolders = [...folderMap.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([folder, count]) => `${folder}/ (${count} files)`);
581
+ const lines = [
582
+ `Generate an onboarding document in ${format} format for new developers joining this codebase.`,
583
+ `Project path: "${blueprint.targetPath}"`,
584
+ `Tone: ${tone}. Length: ${length}.`,
585
+ ``,
586
+ `## Codebase Overview`,
587
+ `- ${blueprint.stats.totalFiles} files across ${Object.keys(blueprint.categories).length} categories`,
588
+ `- Categories: ${JSON.stringify(blueprint.categories)}`,
589
+ ``,
590
+ `## Top-level Folders`,
591
+ ...topFolders.map((f) => `- ${f}`),
592
+ ``,
593
+ `## Entry Points (app-layer files, zero incoming deps)`,
594
+ ...topEntryPoints.map((f) => `- ${f.path}`),
595
+ ``,
596
+ `## Core Shared Files (most imported)`,
597
+ ...topImported.map((f) => `- ${f.path} \u2014 used by ${f.importedByCount ?? 0} files`),
598
+ ``,
599
+ `Include: "Start here" section, key folders tour, important conventions to follow, the 3-5 files to read first, and how to run/test the project.`
600
+ ];
601
+ return lines.join("\n");
602
+ }
603
+ async function generateOnboardingDoc(blueprint, opts, provider) {
604
+ if (!blueprint) {
605
+ return {
606
+ text: NO_DATA_MSG3,
607
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
608
+ };
609
+ }
610
+ const response = await provider.complete({
611
+ systemPrompt: buildSystemPrompt4(opts.tone ?? "friendly"),
612
+ messages: [{ role: "user", content: buildUserPrompt4(blueprint, opts) }],
613
+ maxTokens: 3072
614
+ });
615
+ return {
616
+ text: response.content,
617
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
618
+ };
619
+ }
620
+
621
+ // src/generators/refactoringReport.ts
622
+ var NO_DATA_MSG4 = "No Blueprint data available. Run prism scan first.";
623
+ function buildSystemPrompt5(tone) {
624
+ const toneMap = {
625
+ analytical: "You are a software architecture consultant who produces rigorous, evidence-based refactoring reports with clear prioritization.",
626
+ casual: "You are a senior developer who gives honest, practical refactoring advice based on code structure data.",
627
+ executive: "You are a CTO-level advisor who summarizes architectural debt and refactoring priorities for engineering leadership."
628
+ };
629
+ return `${toneMap[tone] ?? toneMap.analytical} Write only the report \u2014 no preamble, no meta-commentary.`;
630
+ }
631
+ function detectImportCycles(edges) {
632
+ const graph = /* @__PURE__ */ new Map();
633
+ for (const e of edges) {
634
+ if (!graph.has(e.from)) graph.set(e.from, /* @__PURE__ */ new Set());
635
+ graph.get(e.from).add(e.to);
636
+ }
637
+ const cycles = [];
638
+ const visited = /* @__PURE__ */ new Set();
639
+ const stack = /* @__PURE__ */ new Set();
640
+ function dfs(node, path3) {
641
+ if (cycles.length >= 5) return;
642
+ if (stack.has(node)) {
643
+ const cycleStart = path3.indexOf(node);
644
+ if (cycleStart !== -1) {
645
+ cycles.push(path3.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
646
+ }
647
+ return;
648
+ }
649
+ if (visited.has(node)) return;
650
+ visited.add(node);
651
+ stack.add(node);
652
+ for (const neighbor of graph.get(node) ?? []) {
653
+ dfs(neighbor, [...path3, node]);
654
+ }
655
+ stack.delete(node);
656
+ }
657
+ for (const node of graph.keys()) {
658
+ if (!visited.has(node)) dfs(node, []);
659
+ }
660
+ return cycles;
661
+ }
662
+ function buildUserPrompt5(blueprint, opts) {
663
+ const tone = opts.tone ?? "analytical";
664
+ const length = opts.length ?? "medium";
665
+ const format = opts.format ?? "markdown";
666
+ const hotFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) + (b.importCount ?? 0) - ((a.importedByCount ?? 0) + (a.importCount ?? 0))).slice(0, 10);
667
+ const highFanOut = blueprint.files.slice().sort((a, b) => (b.importCount ?? 0) - (a.importCount ?? 0)).slice(0, 8);
668
+ const importCycles = detectImportCycles(blueprint.edges);
669
+ const lines = [
670
+ `Generate a refactoring report in ${format} format for the codebase at "${blueprint.targetPath}".`,
671
+ `Tone: ${tone}. Length: ${length}.`,
672
+ ``,
673
+ `## Architecture Metrics`,
674
+ `- Total files: ${blueprint.stats.totalFiles}`,
675
+ `- Dependency edges: ${blueprint.stats.runtimeEdges}`,
676
+ `- Categories: ${JSON.stringify(blueprint.categories)}`,
677
+ ``,
678
+ `## High-Coupling Hot Spots (high total connections)`,
679
+ ...hotFiles.map((f) => `- ${f.path} \u2014 in: ${f.importedByCount ?? 0}, out: ${f.importCount ?? 0}, lines: ${f.lineCount ?? "?"}`),
680
+ ``,
681
+ `## High Fan-Out Files (many outgoing dependencies)`,
682
+ ...highFanOut.map((f) => `- ${f.path} \u2014 imports: ${f.importCount ?? 0}`),
683
+ ``,
684
+ `## Import Cycles Detected (${importCycles.length})`,
685
+ ...importCycles.length > 0 ? importCycles.map((c) => `- ${c}`) : ["No cycles detected in sampled edges"],
686
+ ``,
687
+ `Identify refactoring priorities: coupling issues, over-large files, circular dependencies, layer violations. Suggest concrete refactoring actions with rationale.`
688
+ ];
689
+ return lines.join("\n");
690
+ }
691
+ async function generateRefactoringReport(blueprint, opts, provider) {
692
+ if (!blueprint) {
693
+ return {
694
+ text: NO_DATA_MSG4,
695
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
696
+ };
697
+ }
698
+ const response = await provider.complete({
699
+ systemPrompt: buildSystemPrompt5(opts.tone ?? "analytical"),
700
+ messages: [{ role: "user", content: buildUserPrompt5(blueprint, opts) }],
701
+ maxTokens: 3072
702
+ });
703
+ return {
704
+ text: response.content,
705
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
706
+ };
707
+ }
708
+
709
+ // src/generators/askDrivenAsset.ts
710
+ var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
711
+ var FORMAT_GUIDES = {
712
+ markdown: { name: "Markdown document", structure: "Use headers, bullet lists, and code blocks where appropriate.", maxTokens: 3072 },
713
+ blog: { name: "blog post", structure: "Write with an engaging intro, clear sections, a conclusion, and a call-to-action.", maxTokens: 3072 },
714
+ social: { name: "social media post", structure: "Write concise, punchy content suitable for Twitter/LinkedIn. Max 280 characters for Twitter mode.", maxTokens: 512 },
715
+ email: { name: "email", structure: "Use Subject:, greeting, body paragraphs, and a sign-off.", maxTokens: 1024 },
716
+ slack: { name: "Slack message", structure: "Keep it conversational, use *bold* for emphasis, bullet points for lists. Max 3 paragraphs.", maxTokens: 512 },
717
+ slide: { name: "presentation outline", structure: "Structure as slide titles with 3-5 bullet points each. Include a title slide and summary slide.", maxTokens: 2048 }
718
+ };
719
+ function buildSystemPrompt6(format, tone) {
720
+ const toneMap = {
721
+ professional: "You are a professional technical writer and developer advocate.",
722
+ casual: "You are a friendly engineering blogger who writes in an approachable, conversational style.",
723
+ technical: "You are a senior software engineer writing precise, implementation-focused content.",
724
+ executive: "You are a VP of Engineering writing high-level, business-value-focused content for leadership."
725
+ };
726
+ const guide = FORMAT_GUIDES[format];
727
+ return `${toneMap[tone] ?? toneMap.professional} Generate a ${guide.name} based on the user's question and the provided codebase architecture context. ${guide.structure} Write only the requested content \u2014 no preamble, no meta-commentary.`;
728
+ }
729
+ function buildUserPrompt6(blueprint, question, opts) {
730
+ const format = opts.format ?? "markdown";
731
+ const tone = opts.tone ?? "professional";
732
+ const length = opts.length ?? "medium";
733
+ const guide = FORMAT_GUIDES[format];
734
+ const lengthGuide = {
735
+ short: "Keep it concise \u2014 1-2 paragraphs or equivalent.",
736
+ medium: "Medium length \u2014 3-5 paragraphs or equivalent.",
737
+ long: "Detailed and comprehensive \u2014 cover the topic thoroughly."
738
+ }[length];
739
+ const topFiles = blueprint.files.slice().sort((a, b) => (b.importedByCount ?? 0) - (a.importedByCount ?? 0)).slice(0, 12);
740
+ const lines = [
741
+ `## User's Question`,
742
+ question,
743
+ ``,
744
+ `## Output Requirements`,
745
+ `- Format: ${guide.name}`,
746
+ `- Tone: ${tone}`,
747
+ `- Length: ${length} (${lengthGuide})`,
748
+ ``,
749
+ `## Codebase Architecture Context`,
750
+ `Target: ${blueprint.targetPath}`,
751
+ `Total files: ${blueprint.stats.totalFiles} | Dependency edges: ${blueprint.stats.runtimeEdges}`,
752
+ `Categories: app=${blueprint.categories.app ?? 0}, components=${blueprint.categories.component ?? 0}, lib=${blueprint.categories.lib ?? 0}, hooks=${blueprint.categories.hook ?? 0}`,
753
+ ``,
754
+ `Key files (by usage):`,
755
+ ...topFiles.map((f) => `- ${f.path} [${f.category ?? "?"}] \u2014 imported by ${f.importedByCount ?? 0} files, ${f.lineCount ?? "?"} lines`)
756
+ ];
757
+ if (blueprint.edges.length > 0) {
758
+ const edgeSample = blueprint.edges.slice(0, 15);
759
+ lines.push(``, `Dependency edges (sample):`, ...edgeSample.map((e) => `- ${e.from} \u2192 ${e.to}`));
760
+ }
761
+ lines.push(``, `Answer the user's question using the architecture context above. Generate the ${guide.name} now.`);
762
+ return lines.join("\n");
763
+ }
764
+ async function generateAskDrivenAsset(blueprint, question, opts, provider) {
765
+ if (!blueprint) {
766
+ return {
767
+ text: NO_DATA_MSG5,
768
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
769
+ };
770
+ }
771
+ if (!question || !question.trim()) {
772
+ return {
773
+ text: "No question provided. Please ask something about your codebase.",
774
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: 0, generator: "forgesmith" }
775
+ };
776
+ }
777
+ const format = opts.format ?? "markdown";
778
+ const tone = opts.tone ?? "professional";
779
+ const maxTokens = FORMAT_GUIDES[format].maxTokens;
780
+ const response = await provider.complete({
781
+ systemPrompt: buildSystemPrompt6(format, tone),
782
+ messages: [{ role: "user", content: buildUserPrompt6(blueprint, question, opts) }],
783
+ maxTokens
784
+ });
785
+ return {
786
+ text: response.content,
787
+ metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
788
+ };
789
+ }
790
+
791
+ // src/forge/types.ts
792
+ var asAudienceId = (id) => id;
793
+
794
+ // src/forge/distill.ts
795
+ var MIN_CLAIMS = 3;
796
+ var MAX_CLAIMS = 6;
797
+ function clampClaims(claims) {
798
+ if (claims.length <= MAX_CLAIMS) return claims;
799
+ return claims.slice(0, MAX_CLAIMS);
800
+ }
801
+ function splitSentences(text) {
802
+ return text.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
803
+ }
804
+ function fromBrief(payload) {
805
+ const body = typeof payload.body === "string" ? payload.body : "";
806
+ return splitSentences(body).slice(0, MAX_CLAIMS);
807
+ }
808
+ function fromKnowledgeRef(payload) {
809
+ const ids = Array.isArray(payload.blockIds) ? payload.blockIds : [];
810
+ const note = typeof payload.note === "string" ? payload.note : "";
811
+ const out = [];
812
+ if (note.trim().length > 0) out.push(note.trim());
813
+ for (const id of ids) {
814
+ out.push(`Knowledge reference: ${id}`);
815
+ if (out.length >= MAX_CLAIMS) break;
816
+ }
817
+ return out;
818
+ }
819
+ function fromGitRange(payload) {
820
+ const summary = payload.summary ?? {};
821
+ const commits = Array.isArray(summary.commits) ? summary.commits : [];
822
+ const subjects = commits.map((c) => typeof c?.subject === "string" ? c.subject : "").filter((s) => s.length > 0);
823
+ const files = typeof summary.files_changed_summary === "string" ? summary.files_changed_summary.split("\n").filter((l) => l.trim().length > 0) : [];
824
+ const out = [];
825
+ if (subjects.length > 0) {
826
+ out.push(`${commits.length} commit(s): ${subjects.slice(0, 3).join("; ")}`);
827
+ }
828
+ if (files.length > 0) {
829
+ const tail = files[files.length - 1] ?? "";
830
+ out.push(`Changes: ${tail.trim()}`);
831
+ }
832
+ for (const subj of subjects.slice(1, MAX_CLAIMS)) {
833
+ out.push(subj);
834
+ if (out.length >= MAX_CLAIMS) break;
835
+ }
836
+ return out;
837
+ }
838
+ function fromAppFeatureRef(payload) {
839
+ const ids = Array.isArray(payload.featureIds) ? payload.featureIds : [];
840
+ const note = typeof payload.note === "string" ? payload.note : "";
841
+ const out = [];
842
+ if (note.trim().length > 0) out.push(note.trim());
843
+ out.push(`References ${ids.length} app feature(s): ${ids.slice(0, 5).join(", ")}`);
844
+ return out;
845
+ }
846
+ function fromBusinessCapabilityRef(payload) {
847
+ const capabilityName = typeof payload.capability_name === "string" ? payload.capability_name : "";
848
+ const resolvedClaims = Array.isArray(payload.resolvedClaims) ? payload.resolvedClaims : [];
849
+ const out = [];
850
+ if (capabilityName.trim().length > 0) {
851
+ out.push(`Business capability: ${capabilityName}`);
852
+ }
853
+ for (const claim of resolvedClaims) {
854
+ if (typeof claim === "string" && claim.trim().length > 0) {
855
+ out.push(claim.trim());
856
+ if (out.length >= MAX_CLAIMS) break;
857
+ }
858
+ }
859
+ return out;
860
+ }
861
+ function buildSourceRefs(signal) {
862
+ const refs = signal.source_refs ?? {};
863
+ const collected = [];
864
+ for (const [key, val] of Object.entries(refs)) {
865
+ if (Array.isArray(val)) {
866
+ for (const item of val) {
867
+ if (typeof item === "string") collected.push(`${key}:${item}`);
868
+ else collected.push(`${key}:${JSON.stringify(item)}`);
869
+ }
870
+ }
871
+ }
872
+ if (collected.length === 0) collected.push(`signal:${signal.id}`);
873
+ return collected;
874
+ }
875
+ function distill(input) {
876
+ const { signal, audience } = input;
877
+ const payload = signal.payload ?? {};
878
+ let statements = [];
879
+ switch (signal.kind) {
880
+ case "brief":
881
+ statements = fromBrief(payload);
882
+ break;
883
+ case "knowledge_ref":
884
+ statements = fromKnowledgeRef(payload);
885
+ break;
886
+ case "git_range":
887
+ statements = fromGitRange(payload);
888
+ break;
889
+ case "app_feature_ref":
890
+ statements = fromAppFeatureRef(payload);
891
+ break;
892
+ case "business_capability_ref":
893
+ statements = fromBusinessCapabilityRef(payload);
894
+ break;
895
+ // amber_capability_ref uses the same pre-resolved payload shape as business_capability_ref.
896
+ case "amber_capability_ref":
897
+ statements = fromBusinessCapabilityRef(payload);
898
+ break;
899
+ // platform_walkthrough is reserved for the platform-adapter; graceful no-op here.
900
+ case "platform_walkthrough":
901
+ statements = [];
902
+ break;
903
+ // green_insight_ref: platform-adapter resolves insight_title + resolvedClaims before distill.
904
+ case "green_insight_ref": {
905
+ const insightTitle = typeof payload.insight_title === "string" ? payload.insight_title : "";
906
+ const resolvedClaims = Array.isArray(payload.resolvedClaims) ? payload.resolvedClaims : [];
907
+ const out = [];
908
+ if (insightTitle.trim().length > 0) {
909
+ out.push(`GREEN insight: ${insightTitle}`);
910
+ }
911
+ for (const c of resolvedClaims) {
912
+ if (typeof c === "string" && c.trim().length > 0) {
913
+ out.push(c.trim());
914
+ }
915
+ }
916
+ statements = out;
917
+ break;
918
+ }
919
+ }
920
+ if (statements.length < MIN_CLAIMS) {
921
+ const fallback = `Signal of kind ${signal.kind} for audience ${audience.id}.`;
922
+ while (statements.length < MIN_CLAIMS) statements.push(fallback);
923
+ }
924
+ const refs = buildSourceRefs(signal);
925
+ const claims = clampClaims(
926
+ statements.map((statement, i) => ({
927
+ id: `c${i + 1}`,
928
+ statement,
929
+ source_ref: refs[i % refs.length] ?? `signal:${signal.id}`
930
+ }))
931
+ );
932
+ return {
933
+ signal_id: signal.id,
934
+ audience_id: String(audience.id),
935
+ audience_label: audience.label,
936
+ tonality: audience.tonality,
937
+ reading_level: audience.reading_level,
938
+ claims,
939
+ raw_source_refs: signal.source_refs ?? {}
940
+ };
941
+ }
942
+
943
+ // src/forge/promptAssembly.ts
944
+ var EXECUTOR_SPICKZETTEL = `You are a precise execution agent. Your role is to carry out a specific step given a fully-specified context.
945
+ - You act on the provided state and knowledge blocks.
946
+ - You return a concrete next_action and state_delta.
947
+ - You never invent facts. If domain rules are missing, you set knowledge_missing.
948
+ - Confidence reflects how certain you are that your action is correct.`;
949
+ var FORGE_WRITER_SPICKZETTEL = `${EXECUTOR_SPICKZETTEL}
950
+
951
+ You write product communication. Your job is to take a structured ProductTruth
952
+ (claims + source refs) and produce a JSON object that fills the template's
953
+ schema_json. Stay inside the audience's tonality and reading level. Never
954
+ invent facts that are not present in the ProductTruth.`;
955
+ function appendJsonSchemaInstruction(systemPrompt, schemaName, schema) {
956
+ const schemaText = JSON.stringify(schema, null, 2);
957
+ const block = [
958
+ "",
959
+ "\u2500\u2500\u2500 OUTPUT CONTRACT \u2500\u2500\u2500",
960
+ `You MUST respond with a single valid JSON object that satisfies the schema named "${schemaName}".`,
961
+ "Return ONLY the JSON object. No prose before or after. No markdown fences.",
962
+ "If a field cannot be filled given the inputs, set it to an empty string or empty array \u2014 do NOT omit required keys.",
963
+ "",
964
+ `Schema (${schemaName}):`,
965
+ schemaText
966
+ ].join("\n");
967
+ return `${systemPrompt}
968
+ ${block}`;
969
+ }
970
+ function tonalityInstruction(audience) {
971
+ return [
972
+ `Audience: ${audience.label} (${audience.id}).`,
973
+ `Tonality: ${audience.tonality}.`,
974
+ `Reading level: ${audience.reading_level}.`,
975
+ `Channel hints: ${audience.channel_hints.join(", ")}.`,
976
+ audience.description
977
+ ].join(" ");
978
+ }
979
+ function formatSpec(resolved) {
980
+ return [
981
+ `Channel: ${resolved.channel}.`,
982
+ `Intent: ${resolved.intent}.`,
983
+ `Format: ${resolved.format}.`,
984
+ `Modality: ${resolved.modality}.`
985
+ ].join(" ");
986
+ }
987
+ function interactiveVisualSlotsBlock(template) {
988
+ const slots = Array.isArray(template.asset_slots) ? template.asset_slots : [];
989
+ const visual = slots.map(
990
+ (raw) => typeof raw === "object" && raw !== null ? raw : null
991
+ ).filter(
992
+ (s) => s !== null && s.modality === "interactive_visual"
993
+ );
994
+ if (visual.length === 0) return null;
995
+ const lines = ["\u2500\u2500\u2500 INTERACTIVE VISUAL SLOTS \u2500\u2500\u2500"];
996
+ const perDocument = [];
997
+ const perSlide = [];
998
+ visual.forEach((slot, i) => {
999
+ const kind = typeof slot.kind === "string" ? slot.kind : `slot_${i}`;
1000
+ const per = typeof slot.per === "string" ? slot.per : "document";
1001
+ const rs = slot.render_spec ?? {};
1002
+ const w = typeof rs.width === "number" ? rs.width : 800;
1003
+ const h = typeof rs.height === "number" ? rs.height : 450;
1004
+ const ar = typeof rs.aspect_ratio === "string" ? rs.aspect_ratio : `${w}:${h}`;
1005
+ lines.push(
1006
+ `\u2022 ${kind} (per ${per}) \u2014 target viewport ${w}\xD7${h} px, aspect ratio ${ar}. The brief you write for this slot will be rendered inside a fixed-dimension widget; describe a layout that reads cleanly at exactly those proportions.`
1007
+ );
1008
+ if (per === "slide") perSlide.push(slot);
1009
+ else perDocument.push(slot);
1010
+ });
1011
+ lines.push("");
1012
+ lines.push("REQUIRED OUTPUT FIELDS for the interactive_visual slots above:");
1013
+ if (perDocument.length > 0) {
1014
+ lines.push(
1015
+ "\u2022 Add a TOP-LEVEL `visual_brief` field (string) to your JSON output. Describe the widget's visual structure, content, and any data values needed to render it. 2\u20136 sentences, concrete, no marketing."
1016
+ );
1017
+ }
1018
+ if (perSlide.length > 0) {
1019
+ lines.push(
1020
+ "\u2022 Each entry of `slides` MUST include a `visual_brief` field (string) describing what the slide-specific widget should draw. Keep each visual_brief 1\u20133 sentences, concrete."
1021
+ );
1022
+ }
1023
+ lines.push(
1024
+ "These fields are in addition to the schema's other required keys; do NOT omit them. Without them the visual cannot be rendered."
1025
+ );
1026
+ return lines.join("\n");
1027
+ }
1028
+ function productTruthBlock(truth) {
1029
+ const claims = truth.claims.map((c) => `- (${c.id}) [${c.source_ref}] ${c.statement}`).join("\n");
1030
+ return ["ProductTruth (deterministic, do not extend):", claims].join("\n");
1031
+ }
1032
+ function assembleForgePrompt(input) {
1033
+ const { template, audience, productTruth, resolved, contextBlocks } = input;
1034
+ const visualBlock = interactiveVisualSlotsBlock(template);
1035
+ const systemBase = [
1036
+ FORGE_WRITER_SPICKZETTEL,
1037
+ "",
1038
+ "\u2500\u2500\u2500 AUDIENCE \u2500\u2500\u2500",
1039
+ tonalityInstruction(audience),
1040
+ "",
1041
+ "\u2500\u2500\u2500 FORMAT \u2500\u2500\u2500",
1042
+ formatSpec(resolved),
1043
+ ...visualBlock ? ["", visualBlock] : []
1044
+ ].join("\n");
1045
+ const schemaName = `forge.${template.id}`;
1046
+ const system = appendJsonSchemaInstruction(systemBase, schemaName, template.schema_json);
1047
+ const blocks = (contextBlocks ?? []).filter(
1048
+ (b) => typeof b === "string" && b.trim().length > 0
1049
+ );
1050
+ const userParts = [];
1051
+ for (const b of blocks) {
1052
+ userParts.push(b);
1053
+ userParts.push("");
1054
+ }
1055
+ userParts.push(productTruthBlock(productTruth));
1056
+ userParts.push("");
1057
+ userParts.push("Produce the JSON object now.");
1058
+ const user = userParts.join("\n");
1059
+ return { system, user, schema_name: schemaName, schema: template.schema_json };
1060
+ }
1061
+
1062
+ // src/forge/validateResult.ts
1063
+ function isStringValue(v) {
1064
+ return typeof v === "string" && v.trim().length > 0;
1065
+ }
1066
+ function isPlainObject(v) {
1067
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1068
+ }
1069
+ function isSchemaSections(s) {
1070
+ return isPlainObject(s) && Array.isArray(s.sections) && s.sections.every((x) => typeof x === "string");
1071
+ }
1072
+ function isSchemaSlides(s) {
1073
+ if (!isPlainObject(s)) return false;
1074
+ const sl = s.slides;
1075
+ return isPlainObject(sl) && typeof sl.min === "number" && typeof sl.max === "number";
1076
+ }
1077
+ function validateAgainstTemplateSchema(schema, candidate) {
1078
+ if (!isPlainObject(candidate)) {
1079
+ return { ok: false, errors: ["result is not a JSON object"] };
1080
+ }
1081
+ if (isSchemaSections(schema)) {
1082
+ const missing = [];
1083
+ for (const key of schema.sections) {
1084
+ if (!isStringValue(candidate[key])) missing.push(key);
1085
+ }
1086
+ if (missing.length > 0) {
1087
+ return {
1088
+ ok: false,
1089
+ errors: [`missing or empty section field(s): ${missing.join(", ")}`]
1090
+ };
1091
+ }
1092
+ return { ok: true, value: candidate };
1093
+ }
1094
+ if (isSchemaSlides(schema)) {
1095
+ const slides = candidate.slides;
1096
+ if (!Array.isArray(slides)) {
1097
+ return { ok: false, errors: ["slides must be an array"] };
1098
+ }
1099
+ const { min, max } = schema.slides;
1100
+ if (slides.length < min || slides.length > max) {
1101
+ return {
1102
+ ok: false,
1103
+ errors: [`slides length ${slides.length} not in [${min}, ${max}]`]
1104
+ };
1105
+ }
1106
+ const slideErrors = [];
1107
+ slides.forEach((s, i) => {
1108
+ if (!isPlainObject(s)) {
1109
+ slideErrors.push(`slide[${i}] not an object`);
1110
+ return;
1111
+ }
1112
+ const hasBody = isStringValue(s.body);
1113
+ const hasHeadline = isStringValue(s.headline);
1114
+ if (!hasBody && !hasHeadline) {
1115
+ slideErrors.push(`slide[${i}] requires non-empty body or headline`);
1116
+ }
1117
+ });
1118
+ if (slideErrors.length > 0) {
1119
+ return { ok: false, errors: slideErrors };
1120
+ }
1121
+ return { ok: true, value: candidate };
1122
+ }
1123
+ return {
1124
+ ok: false,
1125
+ errors: ["template schema_json is not in a supported shape"],
1126
+ unsupported: ["unrecognized schema shape"]
1127
+ };
1128
+ }
1129
+ function tryParseJsonObject(raw) {
1130
+ let s = raw.trim();
1131
+ const fenceMatch = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
1132
+ if (fenceMatch) s = fenceMatch[1].trim();
1133
+ const first = s.indexOf("{");
1134
+ const last = s.lastIndexOf("}");
1135
+ if (first === -1 || last === -1 || last <= first) {
1136
+ throw new Error("no JSON object found in model output");
1137
+ }
1138
+ const slice = s.slice(first, last + 1);
1139
+ return JSON.parse(slice);
1140
+ }
1141
+
1142
+ // src/forge/validateSchemaDefinition.ts
1143
+ function isPlainObject2(v) {
1144
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1145
+ }
1146
+ function validateSchemaDefinition(input) {
1147
+ if (!isPlainObject2(input)) {
1148
+ return { valid: false, errors: ["schema_json must be an object"] };
1149
+ }
1150
+ if (typeof input.type !== "string" || input.type.length === 0) {
1151
+ return {
1152
+ valid: false,
1153
+ errors: ["schema_json must declare a top-level `type` (e.g. 'object')"]
1154
+ };
1155
+ }
1156
+ if (input.type === "object" && input.properties !== void 0) {
1157
+ if (!isPlainObject2(input.properties)) {
1158
+ return {
1159
+ valid: false,
1160
+ errors: ["schema_json.properties must be a flat object record"]
1161
+ };
1162
+ }
1163
+ const propErrors = [];
1164
+ for (const [name, def] of Object.entries(input.properties)) {
1165
+ if (!isPlainObject2(def)) {
1166
+ propErrors.push(`properties.${name} must be an object`);
1167
+ continue;
1168
+ }
1169
+ if (typeof def.type !== "string" || def.type.length === 0) {
1170
+ propErrors.push(`properties.${name}.type required`);
1171
+ }
1172
+ }
1173
+ if (propErrors.length > 0) {
1174
+ return { valid: false, errors: propErrors };
1175
+ }
1176
+ }
1177
+ return { valid: true };
1178
+ }
1179
+ function parseAndValidateSchemaDefinition(jsonText) {
1180
+ let parsed;
1181
+ try {
1182
+ parsed = JSON.parse(jsonText);
1183
+ } catch (e) {
1184
+ return {
1185
+ valid: false,
1186
+ errors: [
1187
+ `schema_json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`
1188
+ ]
1189
+ };
1190
+ }
1191
+ const result = validateSchemaDefinition(parsed);
1192
+ if (!result.valid) return result;
1193
+ return { valid: true, parsed };
1194
+ }
1195
+
1196
+ // src/forge/validateAssetSlots.ts
1197
+ var VALID_MODALITIES = [
1198
+ "text",
1199
+ "text_with_visual_brief",
1200
+ "interactive_visual",
1201
+ "image",
1202
+ "video",
1203
+ "mixed"
1204
+ ];
1205
+ var VALID_PER = /* @__PURE__ */ new Set(["document", "slide"]);
1206
+ var ASPECT_RATIO_RE = /^\d+:\d+$/;
1207
+ var MIN_W = 320;
1208
+ var MAX_W = 4096;
1209
+ var MIN_H = 240;
1210
+ var MAX_H = 4096;
1211
+ function isPlainObject3(v) {
1212
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1213
+ }
1214
+ function isInt(v) {
1215
+ return typeof v === "number" && Number.isFinite(v) && Number.isInteger(v);
1216
+ }
1217
+ function validateAssetSlots(input) {
1218
+ if (!Array.isArray(input)) {
1219
+ return { valid: false, errors: ["asset_slots must be an array"] };
1220
+ }
1221
+ const errors = [];
1222
+ input.forEach((raw, i) => {
1223
+ if (!isPlainObject3(raw)) {
1224
+ errors.push(`asset_slots[${i}] must be an object`);
1225
+ return;
1226
+ }
1227
+ const kind = raw.kind;
1228
+ if (typeof kind !== "string" || kind.length === 0) {
1229
+ errors.push(`asset_slots[${i}].kind must be a non-empty string`);
1230
+ }
1231
+ const per = raw.per;
1232
+ if (typeof per !== "string" || !VALID_PER.has(per)) {
1233
+ errors.push(
1234
+ `asset_slots[${i}].per must be 'document' or 'slide' (got ${JSON.stringify(per)})`
1235
+ );
1236
+ }
1237
+ const modality = raw.modality;
1238
+ if (typeof modality !== "string" || !VALID_MODALITIES.includes(modality)) {
1239
+ errors.push(
1240
+ `asset_slots[${i}].modality invalid (got ${JSON.stringify(modality)})`
1241
+ );
1242
+ }
1243
+ if (modality === "interactive_visual") {
1244
+ const rs = raw.render_spec;
1245
+ if (!isPlainObject3(rs)) {
1246
+ errors.push(
1247
+ `asset_slots[${i}].render_spec required when modality='interactive_visual'`
1248
+ );
1249
+ } else {
1250
+ const ar = rs.aspect_ratio;
1251
+ if (typeof ar !== "string" || !ASPECT_RATIO_RE.test(ar)) {
1252
+ errors.push(
1253
+ `asset_slots[${i}].render_spec.aspect_ratio must match \\d+:\\d+`
1254
+ );
1255
+ }
1256
+ const w = rs.width;
1257
+ if (!isInt(w) || w < MIN_W || w > MAX_W) {
1258
+ errors.push(
1259
+ `asset_slots[${i}].render_spec.width must be an integer in [${MIN_W}, ${MAX_W}]`
1260
+ );
1261
+ }
1262
+ const h = rs.height;
1263
+ if (!isInt(h) || h < MIN_H || h > MAX_H) {
1264
+ errors.push(
1265
+ `asset_slots[${i}].render_spec.height must be an integer in [${MIN_H}, ${MAX_H}]`
1266
+ );
1267
+ }
1268
+ if (rs.animated !== void 0 && typeof rs.animated !== "boolean") {
1269
+ errors.push(
1270
+ `asset_slots[${i}].render_spec.animated must be a boolean when present`
1271
+ );
1272
+ }
1273
+ }
1274
+ }
1275
+ });
1276
+ return errors.length === 0 ? { valid: true } : { valid: false, errors };
1277
+ }
1278
+
1279
+ // src/forge/animationChoice.ts
1280
+ function isPlainObject4(v) {
1281
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1282
+ }
1283
+ function templateAnimatedDefault(assetSlots) {
1284
+ if (!Array.isArray(assetSlots)) return false;
1285
+ for (const raw of assetSlots) {
1286
+ if (!isPlainObject4(raw)) continue;
1287
+ if (raw.modality !== "interactive_visual") continue;
1288
+ const rs = raw.render_spec;
1289
+ if (isPlainObject4(rs) && rs.animated === true) return true;
1290
+ }
1291
+ return false;
1292
+ }
1293
+ function resolveAnimatedChoice(templateDefault, override) {
1294
+ return typeof override === "boolean" ? override : templateDefault;
1295
+ }
1296
+
1297
+ // src/forge/animationDuration.ts
1298
+ var DEFAULT_ANIMATION_DURATION_SECONDS = 10;
1299
+ var ANIMATION_DURATION_PRESETS = [5, 10, 20, 30];
1300
+ var MIN_ANIMATION_DURATION_SECONDS = 1;
1301
+ var MAX_ANIMATION_DURATION_SECONDS = 120;
1302
+ function isUsableDuration(v) {
1303
+ return typeof v === "number" && Number.isFinite(v) && v > 0;
1304
+ }
1305
+ function clampAnimationDuration(seconds) {
1306
+ if (!Number.isFinite(seconds)) return DEFAULT_ANIMATION_DURATION_SECONDS;
1307
+ return Math.min(
1308
+ MAX_ANIMATION_DURATION_SECONDS,
1309
+ Math.max(MIN_ANIMATION_DURATION_SECONDS, Math.round(seconds))
1310
+ );
1311
+ }
1312
+ function resolveAnimationDuration(override, persisted, fallback = DEFAULT_ANIMATION_DURATION_SECONDS) {
1313
+ if (isUsableDuration(override)) return clampAnimationDuration(override);
1314
+ if (isUsableDuration(persisted)) return clampAnimationDuration(persisted);
1315
+ return clampAnimationDuration(fallback);
1316
+ }
1317
+
1318
+ // src/forge/refineLimit.ts
1319
+ var REFINE_SESSION_SOFT_CAP = 100;
1320
+ var REFINE_COUNTDOWN_THRESHOLD = 90;
1321
+ function refineLimitState(usedRefines, softCap = REFINE_SESSION_SOFT_CAP) {
1322
+ const used = Number.isFinite(usedRefines) && usedRefines > 0 ? Math.floor(usedRefines) : 0;
1323
+ const remaining = Math.max(0, softCap - used);
1324
+ return {
1325
+ used,
1326
+ capped: used >= softCap,
1327
+ remaining,
1328
+ showCountdown: used >= REFINE_COUNTDOWN_THRESHOLD
1329
+ };
1330
+ }
1331
+
1332
+ // src/forge/brandKit.ts
1333
+ var DEFAULT_BRAND_KIT_PALETTE = {
1334
+ primary: "#f97316",
1335
+ secondary: "#1e293b",
1336
+ accent: "#fb923c",
1337
+ surface: "#0f172a",
1338
+ text: "#f1f5f9",
1339
+ muted: "#64748b"
1340
+ };
1341
+ var DEFAULT_BRAND_KIT_FONTS = {
1342
+ heading: "Inter",
1343
+ body: "Inter",
1344
+ mono: "JetBrains Mono"
1345
+ };
1346
+ var DEFAULT_BRAND_KIT_VOICE = {
1347
+ tone: "professional",
1348
+ audience: "developers",
1349
+ vocabulary: [],
1350
+ avoid: [],
1351
+ formality: 60,
1352
+ technicality: 70
1353
+ };
1354
+ function defaultBrandKit(partial) {
1355
+ return {
1356
+ name: "My Brand",
1357
+ source: "manual",
1358
+ palette: { ...DEFAULT_BRAND_KIT_PALETTE },
1359
+ fonts: { ...DEFAULT_BRAND_KIT_FONTS },
1360
+ voice: { ...DEFAULT_BRAND_KIT_VOICE },
1361
+ logo: {},
1362
+ version: 1,
1363
+ versions: [],
1364
+ ...partial
1365
+ };
1366
+ }
1367
+ function assembleBrandUrlExtractionPrompt(hints) {
1368
+ const lines = [
1369
+ `Extract a brand kit from the following website metadata.`,
1370
+ `URL: ${hints.url}`
1371
+ ];
1372
+ if (hints.pageTitle) lines.push(`Page title: ${hints.pageTitle}`);
1373
+ if (hints.metaDescription) lines.push(`Meta description: ${hints.metaDescription}`);
1374
+ if (hints.themeColor) lines.push(`Theme color: ${hints.themeColor}`);
1375
+ if (hints.cssColorHints?.length) lines.push(`Color hints from CSS/HTML: ${hints.cssColorHints.join(", ")}`);
1376
+ if (hints.fontHints?.length) lines.push(`Font hints: ${hints.fontHints.join(", ")}`);
1377
+ lines.push(
1378
+ "",
1379
+ "Return a JSON object with exactly this structure (use hex colors, fill all fields with your best inference):",
1380
+ JSON.stringify({
1381
+ name: "<brand name>",
1382
+ palette: {
1383
+ primary: "#rrggbb",
1384
+ secondary: "#rrggbb",
1385
+ accent: "#rrggbb",
1386
+ surface: "#rrggbb",
1387
+ text: "#rrggbb",
1388
+ muted: "#rrggbb"
1389
+ },
1390
+ fonts: {
1391
+ heading: "<font name>",
1392
+ body: "<font name>",
1393
+ mono: "<font name or 'monospace'>"
1394
+ },
1395
+ voice: {
1396
+ tone: "<professional|technical|casual|inspirational|authoritative>",
1397
+ audience: "<developers|executives|general|designers|business>",
1398
+ vocabulary: ["<preferred term>"],
1399
+ avoid: ["<term to avoid>"],
1400
+ formality: 70,
1401
+ technicality: 50
1402
+ },
1403
+ logo: { url: "<logo url if known or empty string>" }
1404
+ }, null, 2),
1405
+ "",
1406
+ "Respond with ONLY the JSON object, no markdown fences, no explanation."
1407
+ );
1408
+ return lines.join("\n");
1409
+ }
1410
+ function isValidHex(v) {
1411
+ return typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v.trim());
1412
+ }
1413
+ function safeHex(v, fallback) {
1414
+ return isValidHex(v) ? v.trim() : fallback;
1415
+ }
1416
+ function safeStr(v, fallback) {
1417
+ return typeof v === "string" && v.trim() ? v.trim() : fallback;
1418
+ }
1419
+ function safeNum(v, fallback) {
1420
+ const n = Number(v);
1421
+ return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : fallback;
1422
+ }
1423
+ function parseBrandKitFromLlmResponse(text, hints) {
1424
+ let raw;
1425
+ const cleaned = text.replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/\s*```\s*$/i, "").trim();
1426
+ try {
1427
+ raw = JSON.parse(cleaned);
1428
+ } catch {
1429
+ const match = /\{[\s\S]*\}/.exec(text);
1430
+ if (match) {
1431
+ try {
1432
+ raw = JSON.parse(match[0]);
1433
+ } catch {
1434
+ raw = {};
1435
+ }
1436
+ } else {
1437
+ raw = {};
1438
+ }
1439
+ }
1440
+ const r = raw && typeof raw === "object" ? raw : {};
1441
+ const palette = r.palette && typeof r.palette === "object" ? r.palette : {};
1442
+ const fonts = r.fonts && typeof r.fonts === "object" ? r.fonts : {};
1443
+ const voice = r.voice && typeof r.voice === "object" ? r.voice : {};
1444
+ const logo = r.logo && typeof r.logo === "object" ? r.logo : {};
1445
+ const vocab = Array.isArray(voice.vocabulary) ? voice.vocabulary.filter((v) => typeof v === "string") : [];
1446
+ const avoid = Array.isArray(voice.avoid) ? voice.avoid.filter((v) => typeof v === "string") : [];
1447
+ return {
1448
+ name: safeStr(r.name, new URL(hints.url).hostname.replace(/^www\./, "")),
1449
+ source: "url",
1450
+ source_url: hints.url,
1451
+ palette: {
1452
+ primary: safeHex(palette.primary, hints.themeColor ?? DEFAULT_BRAND_KIT_PALETTE.primary),
1453
+ secondary: safeHex(palette.secondary, DEFAULT_BRAND_KIT_PALETTE.secondary),
1454
+ accent: safeHex(palette.accent, DEFAULT_BRAND_KIT_PALETTE.accent),
1455
+ surface: safeHex(palette.surface, DEFAULT_BRAND_KIT_PALETTE.surface),
1456
+ text: safeHex(palette.text, DEFAULT_BRAND_KIT_PALETTE.text),
1457
+ muted: safeHex(palette.muted, DEFAULT_BRAND_KIT_PALETTE.muted)
1458
+ },
1459
+ fonts: {
1460
+ heading: safeStr(fonts.heading, DEFAULT_BRAND_KIT_FONTS.heading),
1461
+ body: safeStr(fonts.body, DEFAULT_BRAND_KIT_FONTS.body),
1462
+ mono: safeStr(fonts.mono, DEFAULT_BRAND_KIT_FONTS.mono)
1463
+ },
1464
+ voice: {
1465
+ tone: safeStr(voice.tone, DEFAULT_BRAND_KIT_VOICE.tone),
1466
+ audience: safeStr(voice.audience, DEFAULT_BRAND_KIT_VOICE.audience),
1467
+ vocabulary: vocab,
1468
+ avoid,
1469
+ formality: safeNum(voice.formality, DEFAULT_BRAND_KIT_VOICE.formality),
1470
+ technicality: safeNum(voice.technicality, DEFAULT_BRAND_KIT_VOICE.technicality)
1471
+ },
1472
+ logo: { url: safeStr(logo.url, "") || void 0 },
1473
+ prism_detected: false
1474
+ };
1475
+ }
1476
+
1477
+ // src/forge/brandPalette.ts
1478
+ function isNonEmptyString(v) {
1479
+ return typeof v === "string" && v.trim().length > 0;
1480
+ }
1481
+ function themeEntryToPalette(entry) {
1482
+ if (!entry || typeof entry !== "object") return null;
1483
+ const { primary, accent, bg, text } = entry;
1484
+ if (!isNonEmptyString(primary) || !isNonEmptyString(accent) || !isNonEmptyString(bg) || !isNonEmptyString(text)) {
1485
+ return null;
1486
+ }
1487
+ return {
1488
+ primary: primary.trim(),
1489
+ accent: accent.trim(),
1490
+ background: bg.trim(),
1491
+ card: isNonEmptyString(entry.card) ? entry.card.trim() : "#ffffff",
1492
+ text: text.trim()
1493
+ };
1494
+ }
1495
+ function parseThemeConfigContent(content) {
1496
+ let value = content;
1497
+ if (typeof value === "string") {
1498
+ try {
1499
+ value = JSON.parse(value);
1500
+ } catch {
1501
+ return [];
1502
+ }
1503
+ }
1504
+ if (!Array.isArray(value)) return [];
1505
+ return value.filter(
1506
+ (e) => typeof e === "object" && e !== null && !Array.isArray(e)
1507
+ );
1508
+ }
1509
+ var FORGE_BRAND_THEME_ID = "__forge_brand__";
1510
+ var BRAND_CONTENT_SLOT_KEYS = {
1511
+ primary: "content-primary",
1512
+ accent: "content-accent",
1513
+ bg: "content-bg",
1514
+ card: "content-card",
1515
+ text: "content-text"
1516
+ };
1517
+ function parseBrandThemeContent(content) {
1518
+ let value = content;
1519
+ if (typeof value === "string") {
1520
+ try {
1521
+ value = JSON.parse(value);
1522
+ } catch {
1523
+ return [];
1524
+ }
1525
+ }
1526
+ if (!Array.isArray(value)) return [];
1527
+ return value.filter(
1528
+ (e) => typeof e === "object" && e !== null && !Array.isArray(e)
1529
+ );
1530
+ }
1531
+ function brandThemeConfigToEntry(config) {
1532
+ if (!config || typeof config !== "object") return null;
1533
+ const values = config.values;
1534
+ if (!values || typeof values !== "object") return null;
1535
+ const entry = {
1536
+ id: FORGE_BRAND_THEME_ID,
1537
+ label: config.name ?? "Brand",
1538
+ primary: values[BRAND_CONTENT_SLOT_KEYS.primary],
1539
+ accent: values[BRAND_CONTENT_SLOT_KEYS.accent],
1540
+ bg: values[BRAND_CONTENT_SLOT_KEYS.bg],
1541
+ card: values[BRAND_CONTENT_SLOT_KEYS.card],
1542
+ text: values[BRAND_CONTENT_SLOT_KEYS.text]
1543
+ };
1544
+ return themeEntryToPalette(entry) ? entry : null;
1545
+ }
1546
+ function resolveBrandPalette(input) {
1547
+ if (isNonEmptyString(input.selectedThemeId)) {
1548
+ const picked = input.themes.find((t) => t.id === input.selectedThemeId);
1549
+ const palette = themeEntryToPalette(picked);
1550
+ if (palette) return palette;
1551
+ }
1552
+ return input.scope === "app" ? input.appDefault : input.frameworkDefault;
1553
+ }
1554
+
1555
+ // src/forge/schemaToForm.ts
1556
+ var MULTILINE_KEYWORDS = /body|brief|description|content|text|copy|hashtags|caption|outline|notes|story|paragraph/i;
1557
+ var MULTILINE_MAX_LENGTH = 200;
1558
+ function isPlainObject5(v) {
1559
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1560
+ }
1561
+ function humanise(name) {
1562
+ return name.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
1563
+ }
1564
+ function isMultilineString(name, def) {
1565
+ const max = typeof def.maxLength === "number" ? def.maxLength : void 0;
1566
+ if (max !== void 0) return max > MULTILINE_MAX_LENGTH;
1567
+ return MULTILINE_KEYWORDS.test(name);
1568
+ }
1569
+ function resolveProperty(name, def, required) {
1570
+ const description = typeof def.description === "string" ? def.description : void 0;
1571
+ const label = humanise(name);
1572
+ const t = def.type;
1573
+ if (t === "string") {
1574
+ const enumVals = Array.isArray(def.enum) ? def.enum.filter((x) => typeof x === "string") : void 0;
1575
+ return {
1576
+ kind: "string",
1577
+ name,
1578
+ label,
1579
+ required,
1580
+ description,
1581
+ multiline: enumVals ? false : isMultilineString(name, def),
1582
+ ...enumVals ? { enum: enumVals } : {},
1583
+ ...typeof def.maxLength === "number" ? { maxLength: def.maxLength } : {},
1584
+ ...typeof def.minLength === "number" ? { minLength: def.minLength } : {}
1585
+ };
1586
+ }
1587
+ if (t === "number" || t === "integer") {
1588
+ return {
1589
+ kind: "number",
1590
+ name,
1591
+ label,
1592
+ required,
1593
+ description,
1594
+ integer: t === "integer",
1595
+ ...typeof def.minimum === "number" ? { minimum: def.minimum } : {},
1596
+ ...typeof def.maximum === "number" ? { maximum: def.maximum } : {}
1597
+ };
1598
+ }
1599
+ if (t === "boolean") {
1600
+ return { kind: "boolean", name, label, required, description };
1601
+ }
1602
+ if (t === "array") {
1603
+ const items = isPlainObject5(def.items) ? def.items : null;
1604
+ const itemField = items ? resolveProperty("item", items, true) : null;
1605
+ return {
1606
+ kind: "array",
1607
+ name,
1608
+ label,
1609
+ required,
1610
+ description,
1611
+ itemField,
1612
+ ...typeof def.minItems === "number" ? { minItems: def.minItems } : {},
1613
+ ...typeof def.maxItems === "number" ? { maxItems: def.maxItems } : {}
1614
+ };
1615
+ }
1616
+ if (t === "object" && isPlainObject5(def.properties)) {
1617
+ const reqList = Array.isArray(def.required) ? def.required.filter((x) => typeof x === "string") : [];
1618
+ const fields = [];
1619
+ for (const [k, d] of Object.entries(def.properties)) {
1620
+ if (!isPlainObject5(d)) return null;
1621
+ const f = resolveProperty(k, d, reqList.includes(k));
1622
+ if (!f) return null;
1623
+ fields.push(f);
1624
+ }
1625
+ return { kind: "object", name, label, required, description, fields };
1626
+ }
1627
+ return null;
1628
+ }
1629
+ function schemaToForm(schema) {
1630
+ if (!isPlainObject5(schema)) {
1631
+ return { kind: "raw", reason: "schema_json is not an object" };
1632
+ }
1633
+ if (schema.type === "object" && isPlainObject5(schema.properties)) {
1634
+ const reqList = Array.isArray(schema.required) ? schema.required.filter((x) => typeof x === "string") : [];
1635
+ const fields = [];
1636
+ for (const [name, def] of Object.entries(schema.properties)) {
1637
+ if (!isPlainObject5(def)) {
1638
+ return {
1639
+ kind: "raw",
1640
+ reason: `property '${name}' is not an object \u2014 cannot render`
1641
+ };
1642
+ }
1643
+ const f = resolveProperty(name, def, reqList.includes(name));
1644
+ if (!f) {
1645
+ return {
1646
+ kind: "raw",
1647
+ reason: `property '${name}' uses an unsupported type \u2014 cannot render`
1648
+ };
1649
+ }
1650
+ fields.push(f);
1651
+ }
1652
+ if (fields.length === 0) {
1653
+ return { kind: "raw", reason: "schema has no properties to render" };
1654
+ }
1655
+ return { kind: "form", fields };
1656
+ }
1657
+ if (Array.isArray(schema.sections)) {
1658
+ const sections = schema.sections.filter(
1659
+ (x) => typeof x === "string"
1660
+ );
1661
+ if (sections.length === 0) {
1662
+ return { kind: "raw", reason: "sections array is empty" };
1663
+ }
1664
+ const fields = sections.map((name) => ({
1665
+ kind: "string",
1666
+ name,
1667
+ label: humanise(name),
1668
+ required: true,
1669
+ multiline: MULTILINE_KEYWORDS.test(name)
1670
+ }));
1671
+ return { kind: "form", fields };
1672
+ }
1673
+ return { kind: "raw", reason: "schema shape not form-compatible" };
1674
+ }
1675
+ function defaultValueForField(field) {
1676
+ switch (field.kind) {
1677
+ case "string":
1678
+ return "";
1679
+ case "number":
1680
+ return "";
1681
+ case "boolean":
1682
+ return false;
1683
+ case "array":
1684
+ return [];
1685
+ case "object":
1686
+ return initialFormValues(field.fields, {});
1687
+ }
1688
+ }
1689
+ function pickInitialValue(field, current) {
1690
+ switch (field.kind) {
1691
+ case "string":
1692
+ return typeof current === "string" ? current : "";
1693
+ case "number":
1694
+ return typeof current === "number" ? current : "";
1695
+ case "boolean":
1696
+ return typeof current === "boolean" ? current : false;
1697
+ case "array":
1698
+ return Array.isArray(current) ? current.slice() : [];
1699
+ case "object": {
1700
+ const c = isPlainObject5(current) ? current : {};
1701
+ return initialFormValues(field.fields, c);
1702
+ }
1703
+ }
1704
+ }
1705
+ function initialFormValues(fields, source) {
1706
+ const out = {};
1707
+ for (const f of fields) {
1708
+ out[f.name] = source[f.name] !== void 0 ? pickInitialValue(f, source[f.name]) : defaultValueForField(f);
1709
+ }
1710
+ return out;
1711
+ }
1712
+ function isEmptyString(v) {
1713
+ return typeof v === "string" && v.trim().length === 0;
1714
+ }
1715
+ function validateField(field, value, path3) {
1716
+ const errors = {};
1717
+ switch (field.kind) {
1718
+ case "string": {
1719
+ if (field.required && (value === void 0 || value === null || isEmptyString(value))) {
1720
+ errors[path3] = `${field.label} ist erforderlich`;
1721
+ break;
1722
+ }
1723
+ if (typeof value === "string") {
1724
+ if (field.minLength !== void 0 && value.length < field.minLength) {
1725
+ errors[path3] = `${field.label} mindestens ${field.minLength} Zeichen`;
1726
+ } else if (field.maxLength !== void 0 && value.length > field.maxLength) {
1727
+ errors[path3] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
1728
+ } else if (field.enum && value.length > 0 && !field.enum.includes(value)) {
1729
+ errors[path3] = `${field.label}: Wert nicht in der Auswahl`;
1730
+ }
1731
+ }
1732
+ break;
1733
+ }
1734
+ case "number": {
1735
+ if (value === "" || value === void 0 || value === null) {
1736
+ if (field.required) errors[path3] = `${field.label} ist erforderlich`;
1737
+ break;
1738
+ }
1739
+ const n = typeof value === "number" ? value : Number(value);
1740
+ if (!Number.isFinite(n)) {
1741
+ errors[path3] = `${field.label} muss eine Zahl sein`;
1742
+ } else if (field.integer && !Number.isInteger(n)) {
1743
+ errors[path3] = `${field.label} muss eine ganze Zahl sein`;
1744
+ } else if (field.minimum !== void 0 && n < field.minimum) {
1745
+ errors[path3] = `${field.label} >= ${field.minimum}`;
1746
+ } else if (field.maximum !== void 0 && n > field.maximum) {
1747
+ errors[path3] = `${field.label} <= ${field.maximum}`;
1748
+ }
1749
+ break;
1750
+ }
1751
+ case "boolean":
1752
+ if (field.required && value !== true) {
1753
+ errors[path3] = `${field.label} muss aktiviert sein`;
1754
+ }
1755
+ break;
1756
+ case "array": {
1757
+ const arr = Array.isArray(value) ? value : [];
1758
+ if (field.required && arr.length === 0) {
1759
+ errors[path3] = `${field.label}: mindestens ein Eintrag erforderlich`;
1760
+ }
1761
+ if (field.minItems !== void 0 && arr.length < field.minItems) {
1762
+ errors[path3] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
1763
+ } else if (field.maxItems !== void 0 && arr.length > field.maxItems) {
1764
+ errors[path3] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
1765
+ }
1766
+ if (field.itemField) {
1767
+ arr.forEach((item, i) => {
1768
+ const sub = validateField(
1769
+ field.itemField,
1770
+ item,
1771
+ `${path3}[${i}]`
1772
+ );
1773
+ Object.assign(errors, sub);
1774
+ });
1775
+ }
1776
+ break;
1777
+ }
1778
+ case "object": {
1779
+ const obj = isPlainObject5(value) ? value : {};
1780
+ for (const sub of field.fields) {
1781
+ const subErrors = validateField(sub, obj[sub.name], `${path3}.${sub.name}`);
1782
+ Object.assign(errors, subErrors);
1783
+ }
1784
+ break;
1785
+ }
1786
+ }
1787
+ return errors;
1788
+ }
1789
+ function validateFormValues(fields, values) {
1790
+ const errors = {};
1791
+ for (const f of fields) {
1792
+ Object.assign(errors, validateField(f, values[f.name], f.name));
1793
+ }
1794
+ return errors;
1795
+ }
1796
+ var TEMPLATE_SCHEMA_EXAMPLES = {
1797
+ long_article: {
1798
+ type: "object",
1799
+ required: ["headline", "lede", "sections"],
1800
+ properties: {
1801
+ headline: { type: "string" },
1802
+ lede: { type: "string" },
1803
+ sections: {
1804
+ type: "array",
1805
+ items: {
1806
+ type: "object",
1807
+ properties: { h2: { type: "string" }, body: { type: "string" } },
1808
+ required: ["h2", "body"]
1809
+ }
1810
+ }
1811
+ }
1812
+ },
1813
+ short_post: {
1814
+ type: "object",
1815
+ required: ["hook", "body"],
1816
+ properties: {
1817
+ hook: { type: "string", maxLength: 200 },
1818
+ body: { type: "string", maxLength: 1200 },
1819
+ cta: { type: "string" }
1820
+ }
1821
+ },
1822
+ carousel_sequence: {
1823
+ type: "object",
1824
+ required: ["slides"],
1825
+ properties: {
1826
+ slides: {
1827
+ type: "array",
1828
+ items: {
1829
+ type: "object",
1830
+ properties: {
1831
+ headline: { type: "string" },
1832
+ body: { type: "string" },
1833
+ visual_brief: { type: "string" }
1834
+ },
1835
+ required: ["headline", "body"]
1836
+ }
1837
+ }
1838
+ }
1839
+ }
1840
+ };
1841
+ var TEMPLATE_SCHEMA_GENERIC = {
1842
+ type: "object",
1843
+ required: ["body"],
1844
+ properties: { body: { type: "string" } }
1845
+ };
1846
+ function schemaExampleFor(format) {
1847
+ return TEMPLATE_SCHEMA_EXAMPLES[format] ?? TEMPLATE_SCHEMA_GENERIC;
1848
+ }
1849
+
1850
+ // src/forge/engine.ts
1851
+ async function runForgeGeneration(input) {
1852
+ const { storage, provider } = input;
1853
+ const [signal, template] = await Promise.all([
1854
+ storage.getSignal(input.signalId),
1855
+ storage.getTemplate(input.templateId)
1856
+ ]);
1857
+ if (!signal) {
1858
+ return { kind: "error", status: 404, error: "signal not found", field: "signalId" };
1859
+ }
1860
+ if (!template) {
1861
+ return { kind: "error", status: 404, error: "template not found", field: "templateId" };
1862
+ }
1863
+ const resolved = {
1864
+ channel: template.channel,
1865
+ audience_id: input.audienceOverride ?? String(template.audience_id),
1866
+ intent: input.intentOverride ?? template.intent,
1867
+ format: input.formatOverride ?? template.format,
1868
+ modality: input.modalityOverride ?? template.modality
1869
+ };
1870
+ const audience = await storage.getAudience(resolved.audience_id);
1871
+ if (!audience) {
1872
+ return {
1873
+ kind: "error",
1874
+ status: 404,
1875
+ error: "audience not found",
1876
+ field: "audienceOverride"
1877
+ };
1878
+ }
1879
+ const productTruth = distill({
1880
+ signal,
1881
+ audience
1882
+ });
1883
+ const prompt = assembleForgePrompt({
1884
+ template,
1885
+ audience,
1886
+ productTruth,
1887
+ resolved
1888
+ });
1889
+ if (input.dryRun) {
1890
+ return {
1891
+ kind: "dry_run",
1892
+ payload: {
1893
+ dryRun: true,
1894
+ resolved,
1895
+ product_truth: productTruth,
1896
+ prompt: {
1897
+ system: prompt.system,
1898
+ user: prompt.user,
1899
+ schema_name: prompt.schema_name
1900
+ }
1901
+ }
1902
+ };
1903
+ }
1904
+ let rawResponse = "";
1905
+ let parseError = null;
1906
+ let parsed = null;
1907
+ let validation = null;
1908
+ try {
1909
+ const result = await provider.complete({
1910
+ systemPrompt: prompt.system,
1911
+ messages: [{ role: "user", content: prompt.user }],
1912
+ maxTokens: input.maxTokens ?? 4096
1913
+ });
1914
+ rawResponse = result.content;
1915
+ } catch (err) {
1916
+ parseError = err instanceof Error ? err.message : String(err);
1917
+ }
1918
+ if (!parseError) {
1919
+ try {
1920
+ parsed = tryParseJsonObject(rawResponse);
1921
+ validation = validateAgainstTemplateSchema(
1922
+ template.schema_json,
1923
+ parsed
1924
+ );
1925
+ } catch (e) {
1926
+ parseError = e instanceof Error ? e.message : String(e);
1927
+ }
1928
+ }
1929
+ const status = parseError === null && validation && validation.ok ? "ready" : "failed";
1930
+ const result_json = status === "ready" ? { content: parsed, product_truth: productTruth } : {
1931
+ failure_reason: parseError ?? (validation && !validation.ok ? validation.errors.join("; ") : "unknown"),
1932
+ raw: rawResponse.slice(0, 4e3)
1933
+ };
1934
+ const output = await storage.saveOutput({
1935
+ signal_id: signal.id,
1936
+ template_id: template.id,
1937
+ status,
1938
+ scope_kind: signal.scope_kind,
1939
+ app_id: signal.scope_kind === "app" ? signal.app_id ?? null : null,
1940
+ env: signal.scope_kind === "app" ? signal.env ?? null : null,
1941
+ channel: resolved.channel,
1942
+ audience_id: resolved.audience_id,
1943
+ intent: resolved.intent,
1944
+ format: resolved.format,
1945
+ modality: resolved.modality,
1946
+ result_json
1947
+ });
1948
+ return {
1949
+ kind: "generated",
1950
+ status: status === "ready" ? 201 : 200,
1951
+ payload: {
1952
+ output,
1953
+ validation: validation ?? null,
1954
+ failure_reason: status === "failed" ? result_json.failure_reason ?? null : null
1955
+ }
1956
+ };
1957
+ }
1958
+
1959
+ // src/forge/widget.ts
1960
+ function getSlotValue(slots, template, slotId) {
1961
+ const val = slots[slotId];
1962
+ if (val !== void 0 && val !== null && val !== "") return String(val);
1963
+ const def = template.slots.find((s) => s.id === slotId);
1964
+ return def?.default !== void 0 ? String(def.default) : "";
1965
+ }
1966
+
1967
+ // src/forge/widgetStyle.ts
1968
+ var BASE_SPACING = {
1969
+ xs: "4px",
1970
+ sm: "8px",
1971
+ md: "16px",
1972
+ lg: "24px",
1973
+ xl: "40px"
1974
+ };
1975
+ var STYLE_PRESET_DEFAULT = {
1976
+ id: "default",
1977
+ name: "Default",
1978
+ preset: true,
1979
+ tokens: {
1980
+ radius: { sm: "6px", md: "12px", lg: "20px", full: "9999px" },
1981
+ shadow: {
1982
+ sm: "0 1px 3px rgba(0,0,0,.12)",
1983
+ md: "0 4px 16px rgba(0,0,0,.15)",
1984
+ lg: "0 8px 32px rgba(0,0,0,.20)"
1985
+ },
1986
+ spacing: BASE_SPACING,
1987
+ borderWidth: "1px",
1988
+ borderStyle: "solid",
1989
+ animation: { entry: "fade", durationMs: 300 }
1990
+ },
1991
+ version: 1,
1992
+ versions: [],
1993
+ created_at: "",
1994
+ updated_at: ""
1995
+ };
1996
+ var STYLE_PRESET_MINIMAL = {
1997
+ id: "minimal",
1998
+ name: "Minimal",
1999
+ preset: true,
2000
+ tokens: {
2001
+ radius: { sm: "0px", md: "0px", lg: "0px", full: "0px" },
2002
+ shadow: { sm: "none", md: "none", lg: "none" },
2003
+ spacing: BASE_SPACING,
2004
+ borderWidth: "1px",
2005
+ borderStyle: "solid",
2006
+ animation: { entry: "none", durationMs: 0 }
2007
+ },
2008
+ version: 1,
2009
+ versions: [],
2010
+ created_at: "",
2011
+ updated_at: ""
2012
+ };
2013
+ var STYLE_PRESET_GLASSY = {
2014
+ id: "glassy",
2015
+ name: "Glassy",
2016
+ preset: true,
2017
+ tokens: {
2018
+ radius: { sm: "12px", md: "20px", lg: "28px", full: "9999px" },
2019
+ shadow: {
2020
+ sm: "0 2px 8px rgba(0,0,0,.08), inset 0 1px 0 rgba(255,255,255,.06)",
2021
+ md: "0 8px 24px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.08)",
2022
+ lg: "0 16px 48px rgba(0,0,0,.16), inset 0 1px 0 rgba(255,255,255,.10)"
2023
+ },
2024
+ spacing: BASE_SPACING,
2025
+ borderWidth: "1px",
2026
+ borderStyle: "solid",
2027
+ animation: { entry: "scale", durationMs: 250 }
2028
+ },
2029
+ customCss: ".pw-widget{backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);}",
2030
+ version: 1,
2031
+ versions: [],
2032
+ created_at: "",
2033
+ updated_at: ""
2034
+ };
2035
+ var STYLE_PRESET_BRUTALIST = {
2036
+ id: "brutalist",
2037
+ name: "Brutalist",
2038
+ preset: true,
2039
+ tokens: {
2040
+ radius: { sm: "0px", md: "0px", lg: "0px", full: "0px" },
2041
+ shadow: {
2042
+ sm: "2px 2px 0 currentColor",
2043
+ md: "4px 4px 0 currentColor",
2044
+ lg: "6px 6px 0 currentColor"
2045
+ },
2046
+ spacing: BASE_SPACING,
2047
+ borderWidth: "2px",
2048
+ borderStyle: "solid",
2049
+ animation: { entry: "slide", durationMs: 200 }
2050
+ },
2051
+ version: 1,
2052
+ versions: [],
2053
+ created_at: "",
2054
+ updated_at: ""
2055
+ };
2056
+ var BUNDLED_STYLE_PRESETS = [
2057
+ STYLE_PRESET_DEFAULT,
2058
+ STYLE_PRESET_MINIMAL,
2059
+ STYLE_PRESET_GLASSY,
2060
+ STYLE_PRESET_BRUTALIST
2061
+ ];
2062
+ function getStyleById(id) {
2063
+ return BUNDLED_STYLE_PRESETS.find((s) => s.id === id) ?? STYLE_PRESET_DEFAULT;
2064
+ }
2065
+ function safeStr2(v, fallback) {
2066
+ return typeof v === "string" && v.trim() ? v.trim() : fallback;
2067
+ }
2068
+ function safeNum2(v, fallback) {
2069
+ const n = Number(v);
2070
+ return Number.isFinite(n) ? n : fallback;
2071
+ }
2072
+ function parseStyleFromTokensJson(json) {
2073
+ if (!json || typeof json !== "object") return {};
2074
+ const obj = json;
2075
+ const get = (keys) => {
2076
+ let cur = obj;
2077
+ for (const k of keys) {
2078
+ if (!cur || typeof cur !== "object") return void 0;
2079
+ cur = cur[k];
2080
+ }
2081
+ return cur?.value ?? cur;
2082
+ };
2083
+ const radius = {};
2084
+ const shadow = {};
2085
+ const spacing = {};
2086
+ const radiusSm = get(["borderRadius", "sm"]) ?? get(["radius", "sm"]) ?? get(["border-radius", "sm"]);
2087
+ const radiusMd = get(["borderRadius", "md"]) ?? get(["radius", "md"]) ?? get(["border-radius", "md"]);
2088
+ const radiusLg = get(["borderRadius", "lg"]) ?? get(["radius", "lg"]) ?? get(["border-radius", "lg"]);
2089
+ if (radiusSm) radius.sm = safeStr2(radiusSm, "6px");
2090
+ if (radiusMd) radius.md = safeStr2(radiusMd, "12px");
2091
+ if (radiusLg) radius.lg = safeStr2(radiusLg, "20px");
2092
+ const shadowSm = get(["boxShadow", "sm"]) ?? get(["shadow", "sm"]);
2093
+ const shadowMd = get(["boxShadow", "md"]) ?? get(["shadow", "md"]) ?? get(["boxShadow", "DEFAULT"]);
2094
+ const shadowLg = get(["boxShadow", "lg"]) ?? get(["shadow", "lg"]);
2095
+ if (shadowSm) shadow.sm = safeStr2(shadowSm, STYLE_PRESET_DEFAULT.tokens.shadow.sm);
2096
+ if (shadowMd) shadow.md = safeStr2(shadowMd, STYLE_PRESET_DEFAULT.tokens.shadow.md);
2097
+ if (shadowLg) shadow.lg = safeStr2(shadowLg, STYLE_PRESET_DEFAULT.tokens.shadow.lg);
2098
+ const spacingSm = get(["spacing", "sm"]) ?? get(["space", "sm"]);
2099
+ const spacingMd = get(["spacing", "md"]) ?? get(["space", "md"]);
2100
+ const spacingLg = get(["spacing", "lg"]) ?? get(["space", "lg"]);
2101
+ if (spacingSm) spacing.sm = safeStr2(spacingSm, "8px");
2102
+ if (spacingMd) spacing.md = safeStr2(spacingMd, "16px");
2103
+ if (spacingLg) spacing.lg = safeStr2(spacingLg, "24px");
2104
+ const animDuration = get(["animation", "duration"]) ?? get(["motion", "duration"]);
2105
+ return {
2106
+ ...Object.keys(radius).length ? { radius: { ...STYLE_PRESET_DEFAULT.tokens.radius, ...radius } } : {},
2107
+ ...Object.keys(shadow).length ? { shadow: { ...STYLE_PRESET_DEFAULT.tokens.shadow, ...shadow } } : {},
2108
+ ...Object.keys(spacing).length ? { spacing: { ...STYLE_PRESET_DEFAULT.tokens.spacing, ...spacing } } : {},
2109
+ ...animDuration ? { animation: { ...STYLE_PRESET_DEFAULT.tokens.animation, durationMs: safeNum2(animDuration, 300) } } : {}
2110
+ };
2111
+ }
2112
+ function parseStyleFromTailwindConfig(jsSource) {
2113
+ const result = {};
2114
+ const radiusMatch = {};
2115
+ const radiusRx = /['"]?(?:sm|md|lg|DEFAULT)['"]?\s*:\s*['"]([^'"]+)['"]/g;
2116
+ const radiusSection = /borderRadius\s*:\s*\{([^}]+)\}/.exec(jsSource)?.[1] ?? "";
2117
+ let m;
2118
+ while ((m = radiusRx.exec(radiusSection)) !== null) {
2119
+ radiusMatch[m[0].split(":")[0].replace(/['"\s]/g, "")] = m[1];
2120
+ }
2121
+ if (radiusMatch.sm || radiusMatch.md || radiusMatch.lg) {
2122
+ result.radius = {
2123
+ sm: radiusMatch.sm ?? STYLE_PRESET_DEFAULT.tokens.radius.sm,
2124
+ md: radiusMatch.md ?? radiusMatch.DEFAULT ?? STYLE_PRESET_DEFAULT.tokens.radius.md,
2125
+ lg: radiusMatch.lg ?? STYLE_PRESET_DEFAULT.tokens.radius.lg,
2126
+ full: STYLE_PRESET_DEFAULT.tokens.radius.full
2127
+ };
2128
+ }
2129
+ const spacingSection = /spacing\s*:\s*\{([^}]+)\}/.exec(jsSource)?.[1] ?? "";
2130
+ const spMap = {};
2131
+ const spRx = /['"]?(\d+|sm|md|lg|xs|xl)['"]?\s*:\s*['"]([^'"]+)['"]/g;
2132
+ while ((m = spRx.exec(spacingSection)) !== null) {
2133
+ spMap[m[1]] = m[2];
2134
+ }
2135
+ if (Object.keys(spMap).length) {
2136
+ result.spacing = {
2137
+ xs: spMap.xs ?? spMap["1"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.xs,
2138
+ sm: spMap.sm ?? spMap["2"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.sm,
2139
+ md: spMap.md ?? spMap["4"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.md,
2140
+ lg: spMap.lg ?? spMap["6"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.lg,
2141
+ xl: spMap.xl ?? spMap["10"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.xl
2142
+ };
2143
+ }
2144
+ return result;
2145
+ }
2146
+ function parseStyleFromCss(css) {
2147
+ const tokens = {};
2148
+ const radiusSm = /--radius-sm\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
2149
+ const radiusMd = /--radius(?:-md)?\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
2150
+ const radiusLg = /--radius-lg\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
2151
+ if (radiusSm || radiusMd || radiusLg) {
2152
+ tokens.radius = {
2153
+ sm: radiusSm ?? STYLE_PRESET_DEFAULT.tokens.radius.sm,
2154
+ md: radiusMd ?? STYLE_PRESET_DEFAULT.tokens.radius.md,
2155
+ lg: radiusLg ?? STYLE_PRESET_DEFAULT.tokens.radius.lg,
2156
+ full: STYLE_PRESET_DEFAULT.tokens.radius.full
2157
+ };
2158
+ }
2159
+ const shadowMd = /--shadow(?:-md)?\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
2160
+ if (shadowMd) {
2161
+ tokens.shadow = { ...STYLE_PRESET_DEFAULT.tokens.shadow, md: shadowMd };
2162
+ }
2163
+ return { tokens, customCss: css };
2164
+ }
2165
+
2166
+ // src/forge/widgetTemplates.ts
2167
+ var WIDGET_TEMPLATE_STAT_CARD = {
2168
+ id: "stat-card",
2169
+ name: "Stat Card",
2170
+ description: "Big number with label and optional delta arrow.",
2171
+ free_tier: true,
2172
+ exportFormats: ["html", "markdown", "react"],
2173
+ slots: [
2174
+ { id: "value", label: "Value", type: "text", required: true, placeholder: "138", default: "138" },
2175
+ { id: "label", label: "Label", type: "text", required: true, placeholder: "Total Files", default: "Total Files" },
2176
+ { id: "delta", label: "Delta", type: "text", placeholder: "+12%", default: "" },
2177
+ { id: "delta_direction", label: "Delta direction", type: "select", options: ["up", "down", "neutral"], default: "up" },
2178
+ { id: "unit", label: "Unit (optional)", type: "text", placeholder: "files", default: "" }
2179
+ ]
2180
+ };
2181
+ var WIDGET_TEMPLATE_FEATURE_GRID = {
2182
+ id: "feature-grid",
2183
+ name: "Feature Grid",
2184
+ description: "Grid of features with icons or checkmarks.",
2185
+ free_tier: false,
2186
+ exportFormats: ["html", "markdown", "react"],
2187
+ slots: [
2188
+ { id: "title", label: "Title", type: "text", placeholder: "What you get", default: "What you get" },
2189
+ { id: "features", label: "Features (one per line)", type: "multiline", required: true, placeholder: "Zero config\nInstant deploy\nGlobal CDN", default: "Feature one\nFeature two\nFeature three" },
2190
+ { id: "columns", label: "Columns", type: "select", options: ["2", "3"], default: "3" },
2191
+ { id: "icon", label: "Icon prefix", type: "text", placeholder: "\u2713", default: "\u2713" }
2192
+ ]
2193
+ };
2194
+ var WIDGET_TEMPLATE_TESTIMONIAL = {
2195
+ id: "testimonial",
2196
+ name: "Testimonial",
2197
+ description: "Pull-quote with author, role, and optional photo.",
2198
+ free_tier: false,
2199
+ exportFormats: ["html", "markdown", "react"],
2200
+ slots: [
2201
+ { id: "quote", label: "Quote", type: "multiline", required: true, placeholder: "This tool changed how we ship.", default: "This tool changed how we ship." },
2202
+ { id: "author", label: "Author name", type: "text", required: true, placeholder: "Jane Smith", default: "Jane Smith" },
2203
+ { id: "role", label: "Role", type: "text", placeholder: "CTO", default: "CTO" },
2204
+ { id: "company", label: "Company", type: "text", placeholder: "Acme Corp", default: "Acme Corp" },
2205
+ { id: "image_url", label: "Author photo URL", type: "image-url", placeholder: "https://\u2026", default: "" }
2206
+ ]
2207
+ };
2208
+ var WIDGET_TEMPLATE_CTA_BANNER = {
2209
+ id: "cta-banner",
2210
+ name: "CTA Banner",
2211
+ description: "Full-width call-to-action with heading, subtitle, and button.",
2212
+ free_tier: true,
2213
+ exportFormats: ["html", "markdown", "react"],
2214
+ slots: [
2215
+ { id: "heading", label: "Heading", type: "text", required: true, placeholder: "Ship faster.", default: "Ship faster." },
2216
+ { id: "subtitle", label: "Subtitle", type: "text", placeholder: "From blueprint to production in minutes.", default: "From blueprint to production in minutes." },
2217
+ { id: "button_text", label: "Button text", type: "text", required: true, placeholder: "Get started", default: "Get started" },
2218
+ { id: "button_url", label: "Button URL", type: "url", placeholder: "https://\u2026", default: "#" },
2219
+ { id: "align", label: "Alignment", type: "select", options: ["left", "center"], default: "center" }
2220
+ ]
2221
+ };
2222
+ var WIDGET_TEMPLATE_METRIC_BADGE = {
2223
+ id: "metric-badge",
2224
+ name: "Metric Badge",
2225
+ description: "Small inline metric chip \u2014 uptime, users, coverage.",
2226
+ free_tier: true,
2227
+ exportFormats: ["html", "markdown", "react"],
2228
+ slots: [
2229
+ { id: "metric_name", label: "Metric name", type: "text", required: true, placeholder: "Uptime", default: "Uptime" },
2230
+ { id: "value", label: "Value", type: "text", required: true, placeholder: "99.9", default: "99.9" },
2231
+ { id: "unit", label: "Unit", type: "text", placeholder: "%", default: "%" },
2232
+ { id: "color", label: "Color override", type: "color", default: "" }
2233
+ ]
2234
+ };
2235
+ var WIDGET_TEMPLATE_PRICING_TIER = {
2236
+ id: "pricing-tier",
2237
+ name: "Pricing Tier",
2238
+ description: "Plan name, price, feature bullets, and CTA button.",
2239
+ free_tier: false,
2240
+ exportFormats: ["html", "markdown", "react"],
2241
+ slots: [
2242
+ { id: "plan_name", label: "Plan name", type: "text", required: true, placeholder: "Pro", default: "Pro" },
2243
+ { id: "price", label: "Price", type: "text", required: true, placeholder: "\u20AC49", default: "\u20AC49" },
2244
+ { id: "period", label: "Period", type: "text", placeholder: "/month", default: "/month" },
2245
+ { id: "features", label: "Features (one per line)", type: "multiline", required: true, placeholder: "Unlimited projects\nPriority support\nAPI access", default: "Feature one\nFeature two\nFeature three" },
2246
+ { id: "cta_text", label: "CTA text", type: "text", placeholder: "Get Pro", default: "Get Pro" },
2247
+ { id: "cta_url", label: "CTA URL", type: "url", placeholder: "https://\u2026", default: "#" },
2248
+ { id: "highlighted", label: "Highlighted tier", type: "select", options: ["yes", "no"], default: "no" }
2249
+ ]
2250
+ };
2251
+ var WIDGET_TEMPLATE_CHANGELOG_ROW = {
2252
+ id: "changelog-row",
2253
+ name: "Changelog Row",
2254
+ description: "Version, date, and bullet list of changes.",
2255
+ free_tier: false,
2256
+ exportFormats: ["html", "markdown", "react"],
2257
+ slots: [
2258
+ { id: "version", label: "Version", type: "text", required: true, placeholder: "v1.2.0", default: "v1.2.0" },
2259
+ { id: "date", label: "Date", type: "text", required: true, placeholder: "2024-01-15", default: "2024-01-15" },
2260
+ { id: "label", label: "Release label", type: "select", options: ["feat", "fix", "docs", "chore", ""], default: "feat" },
2261
+ { id: "changes", label: "Changes (one per line)", type: "multiline", required: true, placeholder: "Added dark mode\nFixed auth bug\nImproved performance", default: "Added feature X\nFixed bug Y\nImproved performance" }
2262
+ ]
2263
+ };
2264
+ var WIDGET_TEMPLATE_SOCIAL_PROOF = {
2265
+ id: "social-proof",
2266
+ name: "Social Proof",
2267
+ description: "Logo row with optional caption.",
2268
+ free_tier: false,
2269
+ exportFormats: ["html", "markdown", "react"],
2270
+ slots: [
2271
+ { id: "caption", label: "Caption", type: "text", placeholder: "Trusted by engineering teams at", default: "Trusted by engineering teams at" },
2272
+ { id: "logos", label: "Brand names (one per line)", type: "multiline", required: true, placeholder: "Vercel\nStripe\nLinear\nResend", default: "Acme Corp\nBeta Inc\nGamma Ltd" },
2273
+ { id: "show_divider", label: "Show divider", type: "select", options: ["yes", "no"], default: "no" }
2274
+ ]
2275
+ };
2276
+ var BUNDLED_WIDGET_TEMPLATES = [
2277
+ WIDGET_TEMPLATE_STAT_CARD,
2278
+ WIDGET_TEMPLATE_FEATURE_GRID,
2279
+ WIDGET_TEMPLATE_TESTIMONIAL,
2280
+ WIDGET_TEMPLATE_CTA_BANNER,
2281
+ WIDGET_TEMPLATE_METRIC_BADGE,
2282
+ WIDGET_TEMPLATE_PRICING_TIER,
2283
+ WIDGET_TEMPLATE_CHANGELOG_ROW,
2284
+ WIDGET_TEMPLATE_SOCIAL_PROOF
2285
+ ];
2286
+ var FREE_TIER_WIDGET_IDS = BUNDLED_WIDGET_TEMPLATES.filter((t) => t.free_tier).map((t) => t.id);
2287
+ function getWidgetTemplate(id) {
2288
+ return BUNDLED_WIDGET_TEMPLATES.find((t) => t.id === id);
2289
+ }
2290
+
2291
+ // src/forge/widgetRenderer.ts
2292
+ function brandVars(brand) {
2293
+ const p = brand?.palette;
2294
+ const f = brand?.fonts;
2295
+ return {
2296
+ "--pw-brand-primary": p?.primary ?? "#f97316",
2297
+ "--pw-brand-secondary": p?.secondary ?? "#1e293b",
2298
+ "--pw-brand-accent": p?.accent ?? "#fb923c",
2299
+ "--pw-brand-surface": p?.surface ?? "#0f172a",
2300
+ "--pw-brand-text": p?.text ?? "#f1f5f9",
2301
+ "--pw-brand-muted": p?.muted ?? "#64748b",
2302
+ "--pw-font-heading": f?.heading ? `'${f.heading}',sans-serif` : "sans-serif",
2303
+ "--pw-font-body": f?.body ? `'${f.body}',sans-serif` : "sans-serif",
2304
+ "--pw-font-mono": f?.mono ? `'${f.mono}',monospace` : "monospace"
2305
+ };
2306
+ }
2307
+ function styleVars(style) {
2308
+ const t = style.tokens;
2309
+ return {
2310
+ "--pw-radius-sm": t.radius.sm,
2311
+ "--pw-radius-md": t.radius.md,
2312
+ "--pw-radius-lg": t.radius.lg,
2313
+ "--pw-radius-full": t.radius.full,
2314
+ "--pw-shadow-sm": t.shadow.sm,
2315
+ "--pw-shadow-md": t.shadow.md,
2316
+ "--pw-shadow-lg": t.shadow.lg,
2317
+ "--pw-spacing-xs": t.spacing.xs,
2318
+ "--pw-spacing-sm": t.spacing.sm,
2319
+ "--pw-spacing-md": t.spacing.md,
2320
+ "--pw-spacing-lg": t.spacing.lg,
2321
+ "--pw-spacing-xl": t.spacing.xl,
2322
+ "--pw-border-width": t.borderWidth
2323
+ };
2324
+ }
2325
+ function buildCssVarString(vars) {
2326
+ return Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
2327
+ }
2328
+ function buildInlineStyle(vars) {
2329
+ return buildCssVarString(vars);
2330
+ }
2331
+ function escHtml(s) {
2332
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2333
+ }
2334
+ function baseWidgetStyle() {
2335
+ return `.pw-widget{font-family:var(--pw-font-body);color:var(--pw-brand-text);background:var(--pw-brand-surface);border-radius:var(--pw-radius-md);padding:var(--pw-spacing-md);border:var(--pw-border-width) solid rgba(255,255,255,.08);box-sizing:border-box;}`;
2336
+ }
2337
+ function renderStatCardHtml(slots, t) {
2338
+ const value = getSlotValue(slots, t, "value");
2339
+ const label = getSlotValue(slots, t, "label");
2340
+ const delta = getSlotValue(slots, t, "delta");
2341
+ const dir = getSlotValue(slots, t, "delta_direction");
2342
+ const unit = getSlotValue(slots, t, "unit");
2343
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "\u2192";
2344
+ const deltaColor = dir === "up" ? "#22c55e" : dir === "down" ? "#ef4444" : "inherit";
2345
+ return `<div class="pw-stat-value" style="font-size:2.5rem;font-weight:700;line-height:1;font-family:var(--pw-font-heading);color:var(--pw-brand-primary)">${escHtml(value)}${unit ? `<span style="font-size:1rem;font-weight:400;margin-left:4px">${escHtml(unit)}</span>` : ""}</div>
2346
+ <div class="pw-stat-label" style="font-size:.875rem;color:var(--pw-brand-muted);margin-top:var(--pw-spacing-xs)">${escHtml(label)}</div>
2347
+ ${delta ? `<div class="pw-stat-delta" style="font-size:.75rem;color:${deltaColor};margin-top:var(--pw-spacing-xs)">${arrow} ${escHtml(delta)}</div>` : ""}`;
2348
+ }
2349
+ function renderFeatureGridHtml(slots, t) {
2350
+ const title = getSlotValue(slots, t, "title");
2351
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2352
+ const cols = getSlotValue(slots, t, "columns") || "3";
2353
+ const icon = getSlotValue(slots, t, "icon") || "\u2713";
2354
+ return `${title ? `<h3 style="font-size:1.125rem;font-weight:600;margin:0 0 var(--pw-spacing-md);font-family:var(--pw-font-heading);color:var(--pw-brand-text)">${escHtml(title)}</h3>` : ""}
2355
+ <div class="pw-grid" style="display:grid;grid-template-columns:repeat(${escHtml(cols)},1fr);gap:var(--pw-spacing-sm)">
2356
+ ${features.map((f) => ` <div style="display:flex;align-items:flex-start;gap:var(--pw-spacing-xs);font-size:.875rem"><span style="color:var(--pw-brand-accent);flex-shrink:0">${escHtml(icon)}</span><span>${escHtml(f)}</span></div>`).join("\n")}
2357
+ </div>`;
2358
+ }
2359
+ function renderTestimonialHtml(slots, t) {
2360
+ const quote = getSlotValue(slots, t, "quote");
2361
+ const author = getSlotValue(slots, t, "author");
2362
+ const role = getSlotValue(slots, t, "role");
2363
+ const company = getSlotValue(slots, t, "company");
2364
+ const img = getSlotValue(slots, t, "image_url");
2365
+ return `<blockquote style="margin:0;font-size:1.125rem;line-height:1.6;font-style:italic;color:var(--pw-brand-text)">"${escHtml(quote)}"</blockquote>
2366
+ <div class="pw-attribution" style="display:flex;align-items:center;gap:var(--pw-spacing-sm);margin-top:var(--pw-spacing-md)">
2367
+ ${img ? ` <img src="${escHtml(img)}" alt="${escHtml(author)}" style="width:40px;height:40px;border-radius:var(--pw-radius-full);object-fit:cover" />` : ""}
2368
+ <div>
2369
+ <div style="font-weight:600;font-size:.875rem">${escHtml(author)}</div>
2370
+ ${role || company ? `<div style="font-size:.75rem;color:var(--pw-brand-muted)">${[role, company].filter(Boolean).map(escHtml).join(", ")}</div>` : ""}
2371
+ </div>
2372
+ </div>`;
2373
+ }
2374
+ function renderCtaBannerHtml(slots, t) {
2375
+ const heading = getSlotValue(slots, t, "heading");
2376
+ const subtitle = getSlotValue(slots, t, "subtitle");
2377
+ const btnText = getSlotValue(slots, t, "button_text");
2378
+ const btnUrl = getSlotValue(slots, t, "button_url") || "#";
2379
+ const align = getSlotValue(slots, t, "align") || "center";
2380
+ return `<div style="text-align:${escHtml(align)}">
2381
+ <h2 style="margin:0 0 var(--pw-spacing-sm);font-size:1.75rem;font-weight:700;font-family:var(--pw-font-heading);line-height:1.2">${escHtml(heading)}</h2>
2382
+ ${subtitle ? `<p style="margin:0 0 var(--pw-spacing-md);font-size:1rem;color:var(--pw-brand-muted)">${escHtml(subtitle)}</p>` : ""}
2383
+ <a href="${escHtml(btnUrl)}" style="display:inline-block;padding:var(--pw-spacing-sm) var(--pw-spacing-lg);background:var(--pw-brand-primary);color:#fff;border-radius:var(--pw-radius-sm);font-weight:600;text-decoration:none;font-size:.875rem">${escHtml(btnText)}</a>
2384
+ </div>`;
2385
+ }
2386
+ function renderMetricBadgeHtml(slots, t) {
2387
+ const name = getSlotValue(slots, t, "metric_name");
2388
+ const value = getSlotValue(slots, t, "value");
2389
+ const unit = getSlotValue(slots, t, "unit");
2390
+ const color = getSlotValue(slots, t, "color") || "var(--pw-brand-accent)";
2391
+ return `<span style="display:inline-flex;align-items:center;gap:var(--pw-spacing-xs);padding:2px var(--pw-spacing-sm);border-radius:var(--pw-radius-full);border:var(--pw-border-width) solid ${escHtml(color)}20;background:${escHtml(color)}10">
2392
+ <span style="font-size:.75rem;color:var(--pw-brand-muted)">${escHtml(name)}</span>
2393
+ <span style="font-size:.875rem;font-weight:700;color:${escHtml(color)};font-family:var(--pw-font-mono)">${escHtml(value)}${unit ? `<span style="font-size:.7rem;font-weight:400">${escHtml(unit)}</span>` : ""}</span>
2394
+ </span>`;
2395
+ }
2396
+ function renderPricingTierHtml(slots, t) {
2397
+ const plan = getSlotValue(slots, t, "plan_name");
2398
+ const price = getSlotValue(slots, t, "price");
2399
+ const period = getSlotValue(slots, t, "period");
2400
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2401
+ const cta = getSlotValue(slots, t, "cta_text");
2402
+ const ctaUrl = getSlotValue(slots, t, "cta_url") || "#";
2403
+ const highlighted = getSlotValue(slots, t, "highlighted") === "yes";
2404
+ const borderColor = highlighted ? "var(--pw-brand-primary)" : "rgba(255,255,255,.08)";
2405
+ return `<div style="border-color:${borderColor};border-width:${highlighted ? "2px" : "var(--pw-border-width)"}">
2406
+ <div style="font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--pw-brand-muted);margin-bottom:var(--pw-spacing-xs)">${escHtml(plan)}</div>
2407
+ <div style="display:flex;align-items:baseline;gap:2px;margin-bottom:var(--pw-spacing-md)">
2408
+ <span style="font-size:2rem;font-weight:700;font-family:var(--pw-font-heading)">${escHtml(price)}</span>
2409
+ <span style="font-size:.875rem;color:var(--pw-brand-muted)">${escHtml(period)}</span>
2410
+ </div>
2411
+ <ul style="margin:0 0 var(--pw-spacing-md);padding:0;list-style:none;space-y:var(--pw-spacing-xs)">
2412
+ ${features.map((f) => ` <li style="display:flex;align-items:flex-start;gap:var(--pw-spacing-xs);font-size:.875rem;padding:var(--pw-spacing-xs) 0"><span style="color:var(--pw-brand-accent);flex-shrink:0">\u2713</span><span>${escHtml(f)}</span></li>`).join("\n")}
2413
+ </ul>
2414
+ <a href="${escHtml(ctaUrl)}" style="display:block;text-align:center;padding:var(--pw-spacing-sm);background:${highlighted ? "var(--pw-brand-primary)" : "transparent"};color:${highlighted ? "#fff" : "var(--pw-brand-primary)"};border-radius:var(--pw-radius-sm);font-weight:600;text-decoration:none;font-size:.875rem;border:1px solid var(--pw-brand-primary)">${escHtml(cta)}</a>
2415
+ </div>`;
2416
+ }
2417
+ function renderChangelogRowHtml(slots, t) {
2418
+ const version = getSlotValue(slots, t, "version");
2419
+ const date = getSlotValue(slots, t, "date");
2420
+ const label = getSlotValue(slots, t, "label");
2421
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
2422
+ const labelColor = { feat: "#22c55e", fix: "#f59e0b", docs: "#60a5fa", chore: "#94a3b8" };
2423
+ const lc = labelColor[label] ?? "var(--pw-brand-muted)";
2424
+ return `<div style="display:flex;align-items:baseline;gap:var(--pw-spacing-sm);margin-bottom:var(--pw-spacing-sm);flex-wrap:wrap">
2425
+ <span style="font-family:var(--pw-font-mono);font-size:.875rem;font-weight:600;color:var(--pw-brand-primary)">${escHtml(version)}</span>
2426
+ ${label ? `<span style="font-size:.7rem;padding:1px 6px;border-radius:var(--pw-radius-full);background:${lc}20;color:${lc};font-weight:600">${escHtml(label)}</span>` : ""}
2427
+ <span style="font-size:.75rem;color:var(--pw-brand-muted);font-family:var(--pw-font-mono)">${escHtml(date)}</span>
2428
+ </div>
2429
+ <ul style="margin:0;padding:0 0 0 var(--pw-spacing-md);list-style:disc;color:var(--pw-brand-muted)">
2430
+ ${changes.map((c) => ` <li style="font-size:.875rem;line-height:1.5">${escHtml(c)}</li>`).join("\n")}
2431
+ </ul>`;
2432
+ }
2433
+ function renderSocialProofHtml(slots, t) {
2434
+ const caption = getSlotValue(slots, t, "caption");
2435
+ const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
2436
+ const divider = getSlotValue(slots, t, "show_divider") === "yes";
2437
+ return `${caption ? `<p style="text-align:center;font-size:.75rem;text-transform:uppercase;letter-spacing:.08em;color:var(--pw-brand-muted);margin:0 0 var(--pw-spacing-md)">${escHtml(caption)}</p>` : ""}
2438
+ ${divider ? `<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);margin:var(--pw-spacing-sm) 0" />` : ""}
2439
+ <div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--pw-spacing-lg)">
2440
+ ${logos.map((l) => ` <span style="font-weight:600;font-size:.875rem;color:var(--pw-brand-muted);letter-spacing:-.01em">${escHtml(l)}</span>`).join("\n")}
2441
+ </div>`;
2442
+ }
2443
+ var HTML_BODY_FN = {
2444
+ "stat-card": renderStatCardHtml,
2445
+ "feature-grid": renderFeatureGridHtml,
2446
+ "testimonial": renderTestimonialHtml,
2447
+ "cta-banner": renderCtaBannerHtml,
2448
+ "metric-badge": renderMetricBadgeHtml,
2449
+ "pricing-tier": renderPricingTierHtml,
2450
+ "changelog-row": renderChangelogRowHtml,
2451
+ "social-proof": renderSocialProofHtml
2452
+ };
2453
+ function renderStatCardMd(slots, t) {
2454
+ const value = getSlotValue(slots, t, "value");
2455
+ const label = getSlotValue(slots, t, "label");
2456
+ const delta = getSlotValue(slots, t, "delta");
2457
+ const dir = getSlotValue(slots, t, "delta_direction");
2458
+ const unit = getSlotValue(slots, t, "unit");
2459
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
2460
+ return `**${value}${unit ? " " + unit : ""}** \u2014 ${label}${delta ? ` ${arrow} ${delta}` : ""}`;
2461
+ }
2462
+ function renderFeatureGridMd(slots, t) {
2463
+ const title = getSlotValue(slots, t, "title");
2464
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2465
+ const icon = getSlotValue(slots, t, "icon") || "\u2713";
2466
+ return `${title ? `## ${title}
2467
+
2468
+ ` : ""}${features.map((f) => `- ${icon} ${f}`).join("\n")}`;
2469
+ }
2470
+ function renderTestimonialMd(slots, t) {
2471
+ const quote = getSlotValue(slots, t, "quote");
2472
+ const author = getSlotValue(slots, t, "author");
2473
+ const role = getSlotValue(slots, t, "role");
2474
+ const company = getSlotValue(slots, t, "company");
2475
+ const attribution = [author, role, company].filter(Boolean).join(", ");
2476
+ return `> "${quote}"
2477
+ >
2478
+ > \u2014 ${attribution}`;
2479
+ }
2480
+ function renderCtaBannerMd(slots, t) {
2481
+ const heading = getSlotValue(slots, t, "heading");
2482
+ const subtitle = getSlotValue(slots, t, "subtitle");
2483
+ const btnText = getSlotValue(slots, t, "button_text");
2484
+ const btnUrl = getSlotValue(slots, t, "button_url") || "#";
2485
+ return `## ${heading}
2486
+
2487
+ ${subtitle ? subtitle + "\n\n" : ""}[${btnText}](${btnUrl})`;
2488
+ }
2489
+ function renderMetricBadgeMd(slots, t) {
2490
+ const name = getSlotValue(slots, t, "metric_name");
2491
+ const value = getSlotValue(slots, t, "value");
2492
+ const unit = getSlotValue(slots, t, "unit");
2493
+ return `**${name}**: \`${value}${unit}\``;
2494
+ }
2495
+ function renderPricingTierMd(slots, t) {
2496
+ const plan = getSlotValue(slots, t, "plan_name");
2497
+ const price = getSlotValue(slots, t, "price");
2498
+ const period = getSlotValue(slots, t, "period");
2499
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2500
+ const cta = getSlotValue(slots, t, "cta_text");
2501
+ const ctaUrl = getSlotValue(slots, t, "cta_url") || "#";
2502
+ return `### ${plan} \u2014 ${price}${period}
2503
+
2504
+ ${features.map((f) => `- \u2713 ${f}`).join("\n")}
2505
+
2506
+ [${cta}](${ctaUrl})`;
2507
+ }
2508
+ function renderChangelogRowMd(slots, t) {
2509
+ const version = getSlotValue(slots, t, "version");
2510
+ const date = getSlotValue(slots, t, "date");
2511
+ const label = getSlotValue(slots, t, "label");
2512
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
2513
+ return `### ${version}${label ? ` \`${label}\`` : ""} \u2014 ${date}
2514
+
2515
+ ${changes.map((c) => `- ${c}`).join("\n")}`;
2516
+ }
2517
+ function renderSocialProofMd(slots, t) {
2518
+ const caption = getSlotValue(slots, t, "caption");
2519
+ const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
2520
+ return `${caption ? caption + "\n\n" : ""}${logos.join(" \xB7 ")}`;
2521
+ }
2522
+ var MD_FN = {
2523
+ "stat-card": renderStatCardMd,
2524
+ "feature-grid": renderFeatureGridMd,
2525
+ "testimonial": renderTestimonialMd,
2526
+ "cta-banner": renderCtaBannerMd,
2527
+ "metric-badge": renderMetricBadgeMd,
2528
+ "pricing-tier": renderPricingTierMd,
2529
+ "changelog-row": renderChangelogRowMd,
2530
+ "social-proof": renderSocialProofMd
2531
+ };
2532
+ function toReactStyle(vars) {
2533
+ return "{\n" + Object.entries(vars).map(([k, v]) => ` "${k}": "${v.replace(/"/g, '\\"')}"`).join(",\n") + "\n }";
2534
+ }
2535
+ function renderReact(template, slots, brand, style) {
2536
+ const allVars = { ...brandVars(brand), ...styleVars(style) };
2537
+ const componentName = template.id.split("-").map((p) => p[0].toUpperCase() + p.slice(1)).join("");
2538
+ const slotDecls = template.slots.map((s) => {
2539
+ const val = getSlotValue(slots, template, s.id);
2540
+ if (s.type === "number") return ` const ${s.id.replace(/-/g, "_")} = ${Number(val) || 0};`;
2541
+ return ` const ${s.id.replace(/-/g, "_")} = ${JSON.stringify(val)};`;
2542
+ }).join("\n");
2543
+ const htmlBody = (HTML_BODY_FN[template.id]?.(slots, template) ?? "").replace(/`/g, "\\`");
2544
+ return `// forge0x2B \u2014 ${template.name} widget
2545
+ // Generated by forgesmith widget renderer
2546
+
2547
+ const styleVars = ${toReactStyle(allVars)};
2548
+
2549
+ export default function ${componentName}() {
2550
+ ${slotDecls}
2551
+
2552
+ return (
2553
+ <div
2554
+ className="pw-widget pw-${template.id}"
2555
+ style={styleVars}
2556
+ dangerouslySetInnerHTML={{ __html: \`${htmlBody}\` }}
2557
+ />
2558
+ );
2559
+ }
2560
+ `;
2561
+ }
2562
+ function renderWidget(input) {
2563
+ const { template, slots, brandKit, format } = input;
2564
+ const style = !input.style ? STYLE_PRESET_DEFAULT : typeof input.style === "string" ? getStyleById(input.style) : input.style;
2565
+ if (format === "markdown") {
2566
+ const fn = MD_FN[template.id];
2567
+ return fn ? fn(slots, template) : `<!-- ${template.name} -->`;
2568
+ }
2569
+ if (format === "react") {
2570
+ return renderReact(template, slots, brandKit ?? null, style);
2571
+ }
2572
+ const bodyFn = HTML_BODY_FN[template.id];
2573
+ const body = bodyFn ? bodyFn(slots, template) : "";
2574
+ const allVars = { ...brandVars(brandKit ?? null), ...styleVars(style) };
2575
+ const inlineStyle = buildInlineStyle(allVars);
2576
+ const customCss = style.customCss ?? "";
2577
+ const override = style.templateOverrides?.[template.id]?.html;
2578
+ const finalBody = override ?? body;
2579
+ return `<div class="pw-widget pw-${template.id}" style="${inlineStyle}">
2580
+ <style>.pw-widget{${baseWidgetStyle().replace(/^\.pw-widget\{/, "").replace(/\}$/, "")}${customCss ? " " + customCss : ""}</style>
2581
+ ${finalBody}
2582
+ </div>`;
2583
+ }
2584
+
2585
+ // src/forge/fixtures/audiences.ts
2586
+ var FORGE_AUDIENCES = [
2587
+ {
2588
+ id: asAudienceId("heise_technical_editorial"),
2589
+ label: "Heise \u2014 Technische Redaktion",
2590
+ tonality: "sachlich-tief",
2591
+ reading_level: "expert",
2592
+ channel_hints: ["heise"],
2593
+ description: "Fachredaktion mit tiefem technischem Hintergrund; erwartet Belege, Architekturbezug und keine Marketingsprache."
2594
+ },
2595
+ {
2596
+ id: asAudienceId("connect_consumer_tech"),
2597
+ label: "Connect \u2014 Consumer Tech",
2598
+ tonality: "sachlich-zug\xE4nglich",
2599
+ reading_level: "professional",
2600
+ channel_hints: ["connect"],
2601
+ description: "Consumer-Tech-Redaktion mit Fokus auf Alltagstauglichkeit und Vergleichbarkeit gegen\xFCber Marktalternativen."
2602
+ },
2603
+ {
2604
+ id: asAudienceId("investor_seed"),
2605
+ label: "Investor \u2014 Seed",
2606
+ tonality: "ambitioniert-glaubw\xFCrdig",
2607
+ reading_level: "professional",
2608
+ channel_hints: ["internal", "deck", "email"],
2609
+ description: "Seed-Stage Investor; erwartet These, Marktverst\xE4ndnis und glaubw\xFCrdige Traktion in wenigen Abs\xE4tzen."
2610
+ },
2611
+ {
2612
+ id: asAudienceId("investor_growth"),
2613
+ label: "Investor \u2014 Growth",
2614
+ tonality: "n\xFCchtern-direkt",
2615
+ reading_level: "professional",
2616
+ channel_hints: ["internal", "deck", "email"],
2617
+ description: "Growth-Stage Investor; erwartet Kennzahlen, Defensibility und operative Hebel."
2618
+ },
2619
+ {
2620
+ id: asAudienceId("founder_peer"),
2621
+ label: "Founder Peer",
2622
+ tonality: "sachlich-zug\xE4nglich",
2623
+ reading_level: "professional",
2624
+ channel_hints: ["linkedin", "internal"],
2625
+ description: "Gr\xFCnderkollege auf vergleichbarem Reifegrad; Austausch ohne Pitch-Ton."
2626
+ },
2627
+ {
2628
+ id: asAudienceId("b2b_buyer"),
2629
+ label: "B2B Buyer",
2630
+ tonality: "n\xFCchtern-direkt",
2631
+ reading_level: "professional",
2632
+ channel_hints: ["landing", "onepager", "email"],
2633
+ description: "Entscheider in einem Zielunternehmen; bewertet Nutzen, Integration und Risiko."
2634
+ },
2635
+ {
2636
+ id: asAudienceId("end_customer_friendly"),
2637
+ label: "Endkunde \u2014 freundlich",
2638
+ tonality: "warm-zug\xE4nglich",
2639
+ reading_level: "general",
2640
+ channel_hints: ["landing", "instagram", "email"],
2641
+ description: "Endnutzerin oder Endnutzer ohne Vorwissen; sucht direkten Nutzen und Vertrauen."
2642
+ },
2643
+ {
2644
+ id: asAudienceId("partner_business"),
2645
+ label: "Gesch\xE4ftspartner",
2646
+ tonality: "sachlich-zug\xE4nglich",
2647
+ reading_level: "professional",
2648
+ channel_hints: ["email", "onepager", "deck"],
2649
+ description: "Vertriebs- oder Integrationspartner; bewertet Mehrwert f\xFCr die eigene Wertsch\xF6pfung."
2650
+ },
2651
+ {
2652
+ id: asAudienceId("linkedin_founder_post"),
2653
+ label: "LinkedIn \u2014 Founder Post",
2654
+ tonality: "ambitioniert-glaubw\xFCrdig",
2655
+ reading_level: "professional",
2656
+ channel_hints: ["linkedin"],
2657
+ description: "LinkedIn-Publikum eines Gr\xFCnderprofils; erwartet pers\xF6nlichen Ton mit Substanz."
2658
+ },
2659
+ {
2660
+ id: asAudienceId("linkedin_product_post"),
2661
+ label: "LinkedIn \u2014 Product Post",
2662
+ tonality: "sachlich-zug\xE4nglich",
2663
+ reading_level: "professional",
2664
+ channel_hints: ["linkedin"],
2665
+ description: "LinkedIn-Publikum eines Produktprofils; Fokus auf Funktionalit\xE4t und Anwendungsfall."
2666
+ },
2667
+ {
2668
+ id: asAudienceId("instagram_carousel_general"),
2669
+ label: "Instagram \u2014 Carousel allgemein",
2670
+ tonality: "warm-zug\xE4nglich",
2671
+ reading_level: "general",
2672
+ channel_hints: ["instagram"],
2673
+ description: "Allgemeines Instagram-Publikum; visuelle Logik, kurze Hooks, freundlicher Ton."
2674
+ },
2675
+ {
2676
+ id: asAudienceId("friend_explainer"),
2677
+ label: "Freund \u2014 Erkl\xE4rung",
2678
+ tonality: "warm-zug\xE4nglich",
2679
+ reading_level: "lay",
2680
+ channel_hints: ["internal", "email"],
2681
+ description: "Eine fachfremde, nahestehende Person; sucht Verst\xE4ndnis ohne Fachbegriffe."
2682
+ }
2683
+ ];
2684
+
2685
+ // src/forge/fixtures/templates.ts
2686
+ var FORGE_TEMPLATES = [
2687
+ {
2688
+ id: "heise_long_article",
2689
+ channel: "heise",
2690
+ audience_id: asAudienceId("heise_technical_editorial"),
2691
+ intent: "inform",
2692
+ format: "long_article",
2693
+ modality: "text",
2694
+ tonality: "sachlich-tief",
2695
+ schema_json: {
2696
+ sections: ["lede", "context", "architecture", "evidence", "outlook"]
2697
+ },
2698
+ asset_slots: []
2699
+ },
2700
+ {
2701
+ id: "founder_seed_pitch",
2702
+ channel: "internal",
2703
+ audience_id: asAudienceId("investor_seed"),
2704
+ intent: "pitch",
2705
+ format: "pitch_paragraphs",
2706
+ modality: "text",
2707
+ tonality: "ambitioniert-glaubw\xFCrdig",
2708
+ schema_json: { sections: ["thesis", "market", "wedge", "traction", "ask"] },
2709
+ asset_slots: []
2710
+ },
2711
+ {
2712
+ id: "linkedin_founder_post",
2713
+ channel: "linkedin",
2714
+ audience_id: asAudienceId("linkedin_founder_post"),
2715
+ intent: "announce",
2716
+ format: "short_post",
2717
+ modality: "text_with_visual_brief",
2718
+ tonality: "ambitioniert-glaubw\xFCrdig",
2719
+ schema_json: { sections: ["hook", "body", "cta"] },
2720
+ asset_slots: [{ kind: "image_brief", required: true }]
2721
+ },
2722
+ {
2723
+ id: "linkedin_product_carousel",
2724
+ channel: "linkedin",
2725
+ audience_id: asAudienceId("linkedin_product_post"),
2726
+ intent: "educate",
2727
+ format: "carousel_sequence",
2728
+ modality: "text_with_visual_brief",
2729
+ tonality: "sachlich-zug\xE4nglich",
2730
+ schema_json: { slides: { min: 5, max: 8 } },
2731
+ asset_slots: [{ kind: "image_brief", required: true, per: "slide" }]
2732
+ },
2733
+ {
2734
+ id: "instagram_carousel_general",
2735
+ channel: "instagram",
2736
+ audience_id: asAudienceId("instagram_carousel_general"),
2737
+ intent: "celebrate_release",
2738
+ format: "carousel_sequence",
2739
+ modality: "text_with_visual_brief",
2740
+ tonality: "warm-zug\xE4nglich",
2741
+ schema_json: { slides: { min: 4, max: 7 } },
2742
+ asset_slots: [{ kind: "image_brief", required: true, per: "slide" }]
2743
+ },
2744
+ {
2745
+ id: "landing_section",
2746
+ channel: "landing",
2747
+ audience_id: asAudienceId("b2b_buyer"),
2748
+ intent: "convince",
2749
+ format: "landing_block",
2750
+ modality: "text",
2751
+ tonality: "n\xFCchtern-direkt",
2752
+ schema_json: { sections: ["headline", "subhead", "bullets", "cta"] },
2753
+ asset_slots: []
2754
+ },
2755
+ {
2756
+ id: "friend_explainer_short",
2757
+ channel: "internal",
2758
+ audience_id: asAudienceId("friend_explainer"),
2759
+ intent: "explain_to_layperson",
2760
+ format: "short_post",
2761
+ modality: "text",
2762
+ tonality: "warm-zug\xE4nglich",
2763
+ schema_json: {
2764
+ sections: ["analogy", "what_it_does", "why_it_matters"]
2765
+ },
2766
+ asset_slots: []
2767
+ },
2768
+ {
2769
+ id: "connect_consumer_review",
2770
+ channel: "connect",
2771
+ audience_id: asAudienceId("connect_consumer_tech"),
2772
+ intent: "inform",
2773
+ format: "long_article",
2774
+ modality: "text",
2775
+ tonality: "sachlich-zug\xE4nglich",
2776
+ schema_json: {
2777
+ sections: ["intro", "everyday_use", "comparison", "verdict"]
2778
+ },
2779
+ asset_slots: []
2780
+ }
2781
+ ];
2782
+
2783
+ // src/forge/channel.ts
2784
+ var DISPATCH_CHANNEL_TWEET = {
2785
+ id: "tweet",
2786
+ name: "X / Twitter",
2787
+ kind: "tweet",
2788
+ maxLength: 280,
2789
+ promptHints: "Write a single punchy tweet. Use line breaks for rhythm. 1\u20133 relevant hashtags at the end. No thread notation. Conversational, hook-first. No em-dashes\u2014use plain punctuation."
2790
+ };
2791
+ var DISPATCH_CHANNEL_LINKEDIN = {
2792
+ id: "linkedin",
2793
+ name: "LinkedIn",
2794
+ kind: "linkedin",
2795
+ maxLength: 1300,
2796
+ promptHints: "Write a LinkedIn post for a professional audience. Start with a hook sentence on its own line. Use short paragraphs (1\u20133 sentences). End with a clear takeaway or call to action. Avoid buzzword soup. Authentic, peer-to-peer tone."
2797
+ };
2798
+ var DISPATCH_CHANNEL_BLOG = {
2799
+ id: "blog",
2800
+ name: "Blog Post",
2801
+ kind: "blog",
2802
+ maxLength: 3e3,
2803
+ promptHints: "Write the opening section of a blog post: headline, 1\u20132 sentence lede, and 2\u20134 paragraphs expanding the core idea. Use subheadings where helpful. Developer-friendly prose \u2014 concrete, no fluff."
2804
+ };
2805
+ var DISPATCH_CHANNEL_NEWSLETTER = {
2806
+ id: "newsletter",
2807
+ name: "Newsletter",
2808
+ kind: "newsletter",
2809
+ maxLength: 1800,
2810
+ promptHints: "Write a newsletter section (not the full issue). Lead with a brief context sentence, then the announcement, then 2\u20133 bullet points of detail, then a CTA link placeholder '[CTA]'. Conversational but informative. Appropriate for a developer newsletter."
2811
+ };
2812
+ var DISPATCH_CHANNEL_HN = {
2813
+ id: "hn",
2814
+ name: "Hacker News",
2815
+ kind: "hn",
2816
+ maxLength: 800,
2817
+ promptHints: "Write a Show HN or Ask HN submission: a one-line title (max 80 chars, factual, no hype) followed by a short 'I built...' or 'We built...' paragraph explaining the technical motivation and what makes it interesting. HN readers are skeptical \u2014 be honest and specific."
2818
+ };
2819
+ var DISPATCH_CHANNEL_INSTAGRAM = {
2820
+ id: "instagram",
2821
+ name: "Instagram",
2822
+ kind: "instagram",
2823
+ maxLength: 2200,
2824
+ promptHints: "Write an Instagram caption. Start with a hook (first 125 chars are shown before 'more'). Use emojis sparingly and purposefully. Add 5\u201310 relevant hashtags at the end separated from the main text by a blank line. Aspirational but grounded tone."
2825
+ };
2826
+ var DISPATCH_CHANNEL_REDDIT = {
2827
+ id: "reddit",
2828
+ name: "Reddit",
2829
+ kind: "reddit",
2830
+ maxLength: 1e3,
2831
+ promptHints: "Write a Reddit post for a relevant technical subreddit. Title on the first line (max 300 chars), then the body. Be genuine \u2014 Redditors hate marketing. Lead with what it does and why it matters to them, not to you. Invite discussion at the end."
2832
+ };
2833
+ var DISPATCH_CHANNEL_EMAIL = {
2834
+ id: "email",
2835
+ name: "Email",
2836
+ kind: "email",
2837
+ maxLength: 1500,
2838
+ promptHints: "Write a plain-text outreach email. Subject line on the first line prefixed with 'Subject: '. Then greeting, short context (1 sentence), the announcement (2\u20133 sentences), a specific CTA, and sign-off. Professional but personal. No HTML."
2839
+ };
2840
+ var DISPATCH_CHANNEL_SLACK = {
2841
+ id: "slack",
2842
+ name: "Slack",
2843
+ kind: "slack",
2844
+ maxLength: 600,
2845
+ promptHints: "Write a Slack announcement for an internal team channel. Use Slack markdown: *bold*, `code`. Keep it brief \u2014 3\u20135 sentences max. Lead with the key fact, not context. End with a link placeholder '[link]' if relevant."
2846
+ };
2847
+ var BUNDLED_DISPATCH_CHANNELS = [
2848
+ DISPATCH_CHANNEL_TWEET,
2849
+ DISPATCH_CHANNEL_LINKEDIN,
2850
+ DISPATCH_CHANNEL_BLOG,
2851
+ DISPATCH_CHANNEL_NEWSLETTER,
2852
+ DISPATCH_CHANNEL_HN,
2853
+ DISPATCH_CHANNEL_INSTAGRAM,
2854
+ DISPATCH_CHANNEL_REDDIT,
2855
+ DISPATCH_CHANNEL_EMAIL,
2856
+ DISPATCH_CHANNEL_SLACK
2857
+ ];
2858
+ function getDispatchChannel(id) {
2859
+ return BUNDLED_DISPATCH_CHANNELS.find((c) => c.id === id);
2860
+ }
2861
+
2862
+ // src/forge/dispatch.ts
2863
+ function buildSystemPrompt7(channel, audience, brand, blueprintContext, toneOffset) {
2864
+ const lines = [
2865
+ `You are a skilled content writer producing a ${channel.name} post.`,
2866
+ "",
2867
+ `Channel rules:`,
2868
+ `- Max length: ${channel.maxLength} characters (HARD LIMIT \u2014 do not exceed)`,
2869
+ `- ${channel.promptHints}`
2870
+ ];
2871
+ if (audience) {
2872
+ lines.push("", `Audience: ${audience.label}`);
2873
+ lines.push(`Tone: ${audience.tonality}`);
2874
+ lines.push(`Reading level: ${audience.reading_level}`);
2875
+ }
2876
+ if (toneOffset !== void 0 && toneOffset !== 0) {
2877
+ const adj = toneOffset > 0 ? "more formal and professional" : "more casual and conversational";
2878
+ lines.push(`Tone adjustment: ${adj} (offset ${toneOffset > 0 ? "+" : ""}${toneOffset})`);
2879
+ }
2880
+ if (brand) {
2881
+ lines.push("", `Brand voice: ${brand.voice.tone}, formality ${brand.voice.formality}/100`);
2882
+ if (brand.voice.vocabulary && brand.voice.vocabulary.length > 0) {
2883
+ lines.push(`Preferred vocabulary: ${brand.voice.vocabulary.slice(0, 8).join(", ")}`);
2884
+ }
2885
+ if (brand.voice.avoid && brand.voice.avoid.length > 0) {
2886
+ lines.push(`Avoid these words: ${brand.voice.avoid.slice(0, 6).join(", ")}`);
2887
+ }
2888
+ }
2889
+ if (blueprintContext) {
2890
+ lines.push("", "\u2500\u2500\u2500 CODEBASE CONTEXT (from prism blueprint) \u2500\u2500\u2500");
2891
+ lines.push(blueprintContext);
2892
+ lines.push(
2893
+ "You may reference specific metrics above (file counts, zones, churn) to make the post concrete and credible.",
2894
+ "Do not invent metrics that are not in the context."
2895
+ );
2896
+ }
2897
+ lines.push(
2898
+ "",
2899
+ "IMPORTANT: Return ONLY the post content \u2014 no preamble, no 'Here is your post:', no wrapper quotes.",
2900
+ "Stay strictly within the character limit."
2901
+ );
2902
+ return lines.join("\n");
2903
+ }
2904
+ function truncate(content, maxLength) {
2905
+ if (content.length <= maxLength) return { content, truncated: false };
2906
+ const cut = content.slice(0, maxLength);
2907
+ const lastSpace = cut.lastIndexOf(" ");
2908
+ const trimmed = lastSpace > maxLength * 0.8 ? cut.slice(0, lastSpace) : cut;
2909
+ return { content: trimmed.trimEnd(), truncated: true };
2910
+ }
2911
+ async function generateForChannel(ask, channel, provider, opts) {
2912
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2913
+ try {
2914
+ const system = buildSystemPrompt7(
2915
+ channel,
2916
+ opts.audience,
2917
+ opts.brand,
2918
+ opts.blueprintContext,
2919
+ opts.toneOffset
2920
+ );
2921
+ const resp = await provider.complete({
2922
+ systemPrompt: system,
2923
+ messages: [{ role: "user", content: ask }],
2924
+ maxTokens: Math.ceil(channel.maxLength / 3.5) + 100
2925
+ // rough token estimate + buffer
2926
+ });
2927
+ const raw = resp.content.trim();
2928
+ const { content, truncated } = truncate(raw, channel.maxLength);
2929
+ return {
2930
+ output: {
2931
+ channel_id: channel.id,
2932
+ channel_name: channel.name,
2933
+ kind: channel.kind,
2934
+ content,
2935
+ char_count: content.length,
2936
+ truncated,
2937
+ generated_at: now
2938
+ },
2939
+ inputTokens: resp.usedTokens,
2940
+ // approximation — most providers return total
2941
+ outputTokens: 0
2942
+ };
2943
+ } catch (err) {
2944
+ return {
2945
+ output: {
2946
+ channel_id: channel.id,
2947
+ channel_name: channel.name,
2948
+ kind: channel.kind,
2949
+ content: "",
2950
+ char_count: 0,
2951
+ truncated: false,
2952
+ generated_at: now,
2953
+ error: err instanceof Error ? err.message : String(err)
2954
+ },
2955
+ inputTokens: 0,
2956
+ outputTokens: 0
2957
+ };
2958
+ }
2959
+ }
2960
+ async function orchestrateDispatch(input, provider) {
2961
+ const { ask, channels, audience, brand, blueprintContext, toneOverrides = {} } = input;
2962
+ const results = await Promise.all(
2963
+ channels.map(
2964
+ (ch) => generateForChannel(ask, ch, provider, {
2965
+ audience,
2966
+ brand,
2967
+ blueprintContext,
2968
+ toneOffset: toneOverrides[ch.id]
2969
+ })
2970
+ )
2971
+ );
2972
+ const outputs = results.map((r) => r.output);
2973
+ const totalInput = results.reduce((acc, r) => acc + r.inputTokens, 0);
2974
+ const totalOutput = results.reduce((acc, r) => acc + r.outputTokens, 0);
2975
+ return {
2976
+ outputs,
2977
+ usage: {
2978
+ total_input: totalInput,
2979
+ total_output: totalOutput,
2980
+ channel_count: channels.length
2981
+ }
2982
+ };
2983
+ }
2984
+
2985
+ // src/forge/assetVersioning.ts
2986
+ function uuid() {
2987
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2988
+ const r = Math.random() * 16 | 0;
2989
+ return (c === "x" ? r : r & 3 | 8).toString(16);
2990
+ });
2991
+ }
2992
+ function computeDiff(contentA, contentB) {
2993
+ const linesA = contentA.split("\n");
2994
+ const linesB = contentB.split("\n");
2995
+ const m = linesA.length;
2996
+ const n = linesB.length;
2997
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
2998
+ for (let i2 = 1; i2 <= m; i2++) {
2999
+ for (let j2 = 1; j2 <= n; j2++) {
3000
+ if (linesA[i2 - 1] === linesB[j2 - 1]) {
3001
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
3002
+ } else {
3003
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
3004
+ }
3005
+ }
3006
+ }
3007
+ const entries = [];
3008
+ let i = m, j = n;
3009
+ while (i > 0 || j > 0) {
3010
+ if (i > 0 && j > 0 && linesA[i - 1] === linesB[j - 1]) {
3011
+ entries.push({ type: "unchanged", line: linesA[i - 1], lineNo: i });
3012
+ i--;
3013
+ j--;
3014
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
3015
+ entries.push({ type: "added", line: linesB[j - 1] });
3016
+ j--;
3017
+ } else {
3018
+ entries.push({ type: "removed", line: linesA[i - 1], lineNo: i });
3019
+ i--;
3020
+ }
3021
+ }
3022
+ entries.reverse();
3023
+ const addedCount = entries.filter((e) => e.type === "added").length;
3024
+ const removedCount = entries.filter((e) => e.type === "removed").length;
3025
+ return { entries, addedCount, removedCount };
3026
+ }
3027
+ function buildVersion(assetId, versionNumber, content, message, meta) {
3028
+ return {
3029
+ id: uuid(),
3030
+ assetId,
3031
+ versionNumber,
3032
+ content,
3033
+ message,
3034
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3035
+ ...meta
3036
+ };
3037
+ }
3038
+ function buildRevertVersion(assetId, versionNumber, source) {
3039
+ return {
3040
+ id: uuid(),
3041
+ assetId,
3042
+ versionNumber,
3043
+ content: source.content,
3044
+ message: `Reverted to v${source.versionNumber}`,
3045
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3046
+ brandKitId: source.brandKitId,
3047
+ styleId: source.styleId,
3048
+ blueprintFocus: source.blueprintFocus,
3049
+ fromPrism: source.fromPrism,
3050
+ revertedFrom: source.versionNumber
3051
+ };
3052
+ }
3053
+ function nextVersionNumber(versions) {
3054
+ if (versions.length === 0) return 1;
3055
+ return Math.max(...versions.map((v) => v.versionNumber)) + 1;
3056
+ }
3057
+
3058
+ // src/forge/refineAsset.ts
3059
+ function buildSystemPrompt8(input) {
3060
+ const parts = [
3061
+ `You are a content refinement assistant for a developer-focused content tool (forge0x2B).`,
3062
+ `You are refining an existing ${input.assetType.replace(/-/g, " ")} asset.`,
3063
+ ``,
3064
+ `TASK: Apply the user's requested change to the asset content below. Return two things:`,
3065
+ `1. REFINED_CONTENT: The complete revised content (not just the changed parts)`,
3066
+ `2. REPLY: A brief explanation of what you changed and why (1-3 sentences)`,
3067
+ ``,
3068
+ `FORMAT YOUR RESPONSE EXACTLY LIKE THIS (no extra text outside these markers):`,
3069
+ `<REFINED_CONTENT>`,
3070
+ `[the full revised content here]`,
3071
+ `</REFINED_CONTENT>`,
3072
+ `<REPLY>`,
3073
+ `[your brief explanation here]`,
3074
+ `</REPLY>`
3075
+ ];
3076
+ if (input.brandName || input.brandVoice) {
3077
+ parts.push(``, `Brand context:`);
3078
+ if (input.brandName) parts.push(` Name: ${input.brandName}`);
3079
+ if (input.brandVoice) parts.push(` Voice: ${input.brandVoice}`);
3080
+ }
3081
+ if (input.styleHint) {
3082
+ parts.push(` Style: ${input.styleHint}`);
3083
+ }
3084
+ if (input.blueprintContext) {
3085
+ parts.push(``, `Blueprint context (codebase facts to reference):`, input.blueprintContext);
3086
+ }
3087
+ parts.push(``, `CURRENT CONTENT:`, `---`, input.currentContent, `---`);
3088
+ return parts.join("\n");
3089
+ }
3090
+ function parseRefinedResponse(raw) {
3091
+ const contentMatch = raw.match(/<REFINED_CONTENT>([\s\S]*?)<\/REFINED_CONTENT>/);
3092
+ const replyMatch = raw.match(/<REPLY>([\s\S]*?)<\/REPLY>/);
3093
+ const newContent = contentMatch ? contentMatch[1].trim() : raw.trim();
3094
+ const llmReply = replyMatch ? replyMatch[1].trim() : "Content refined as requested.";
3095
+ return { newContent, llmReply };
3096
+ }
3097
+ async function refineAsset(input, provider) {
3098
+ const systemPrompt = buildSystemPrompt8(input);
3099
+ if (scanForSecrets(systemPrompt)) {
3100
+ throw new Error("refineAsset: secret pattern detected in asset content. Refusing to send to LLM.");
3101
+ }
3102
+ if (scanForSecrets(input.userMessage)) {
3103
+ throw new Error("refineAsset: secret pattern detected in user message. Refusing to send to LLM.");
3104
+ }
3105
+ const response = await provider.complete({
3106
+ messages: [{ role: "user", content: input.userMessage }],
3107
+ systemPrompt,
3108
+ maxTokens: 2e3
3109
+ });
3110
+ const { newContent, llmReply } = parseRefinedResponse(response.content);
3111
+ return {
3112
+ newContent,
3113
+ llmReply,
3114
+ usedTokens: response.usedTokens
3115
+ };
3116
+ }
3117
+ var REFINE_SUGGESTIONS = [
3118
+ "Make it more concise",
3119
+ "Add more technical detail",
3120
+ "Make the tone more formal",
3121
+ "Make the tone more casual and conversational",
3122
+ "Add a strong opening hook",
3123
+ "Strengthen the call to action",
3124
+ "Break into shorter paragraphs",
3125
+ "Focus more on developer impact"
3126
+ ];
3127
+ function buildScheduledEntry(assetId, channelId, scheduledFor, contentPreview, metadata = {}) {
3128
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3129
+ return {
3130
+ id: crypto.randomUUID(),
3131
+ assetId,
3132
+ channelId,
3133
+ scheduledFor,
3134
+ status: "draft",
3135
+ contentPreview: contentPreview.slice(0, 60),
3136
+ metadata,
3137
+ createdAt: now,
3138
+ updatedAt: now
3139
+ };
3140
+ }
3141
+ function applyEntryPatch(entry, patch) {
3142
+ return {
3143
+ ...entry,
3144
+ ...patch,
3145
+ metadata: { ...entry.metadata, ...patch.metadata ?? {} },
3146
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3147
+ };
3148
+ }
3149
+ function entryInRange(entry, range) {
3150
+ const d = entry.scheduledFor.slice(0, 10);
3151
+ return d >= range.from && d <= range.to;
3152
+ }
3153
+ function next7DaysRange() {
3154
+ const from = /* @__PURE__ */ new Date();
3155
+ const to = /* @__PURE__ */ new Date();
3156
+ to.setDate(to.getDate() + 6);
3157
+ return {
3158
+ from: from.toISOString().slice(0, 10),
3159
+ to: to.toISOString().slice(0, 10)
3160
+ };
3161
+ }
3162
+ function cascadingScheduledFor(channelIds, startFrom = /* @__PURE__ */ new Date()) {
3163
+ const result = {};
3164
+ channelIds.forEach((id, i) => {
3165
+ const d = new Date(startFrom);
3166
+ d.setDate(d.getDate() + i);
3167
+ d.setHours(9, 0, 0, 0);
3168
+ result[id] = d.toISOString();
3169
+ });
3170
+ return result;
3171
+ }
3172
+
3173
+ // src/forge/exporters.ts
3174
+ function csvEscape(value) {
3175
+ if (value.includes('"') || value.includes(",") || value.includes("\n") || value.includes("\r")) {
3176
+ return `"${value.replace(/"/g, '""')}"`;
3177
+ }
3178
+ return value;
3179
+ }
3180
+ function csvRow(cells) {
3181
+ return cells.map(csvEscape).join(",");
3182
+ }
3183
+ function exportToBufferCsv(entries) {
3184
+ const header = csvRow(["Date", "Time", "Content", "Link", "Image"]);
3185
+ const rows = entries.map((e) => {
3186
+ const d = new Date(e.scheduledFor);
3187
+ const pad = (n) => String(n).padStart(2, "0");
3188
+ const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
3189
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
3190
+ return csvRow([date, time, e.contentPreview, "", ""]);
3191
+ });
3192
+ return [header, ...rows].join("\r\n");
3193
+ }
3194
+ function exportToHypefuryCsv(entries) {
3195
+ const header = csvRow(["scheduled_at", "content", "tweet_type"]);
3196
+ const rows = entries.map(
3197
+ (e) => csvRow([e.scheduledFor, e.contentPreview, "tweet"])
3198
+ );
3199
+ return [header, ...rows].join("\r\n");
3200
+ }
3201
+ function icsDate(iso) {
3202
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}/, "").replace("Z", "Z");
3203
+ }
3204
+ function icsEscape(value) {
3205
+ return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n");
3206
+ }
3207
+ function foldLine(line) {
3208
+ const parts = [];
3209
+ while (line.length > 75) {
3210
+ parts.push(line.slice(0, 75));
3211
+ line = " " + line.slice(75);
3212
+ }
3213
+ parts.push(line);
3214
+ return parts.join("\r\n");
3215
+ }
3216
+ function exportToICalendar(entries) {
3217
+ const now = icsDate((/* @__PURE__ */ new Date()).toISOString());
3218
+ const events = entries.map((e) => {
3219
+ const dtStart = icsDate(e.scheduledFor);
3220
+ const end = new Date(new Date(e.scheduledFor).getTime() + 30 * 60 * 1e3);
3221
+ const dtEnd = icsDate(end.toISOString());
3222
+ const summary = icsEscape(`[${e.channelId}] ${e.contentPreview}`);
3223
+ const description = icsEscape(
3224
+ `Asset: ${e.assetId} \xB7 Channel: ${e.channelId} \xB7 Status: ${e.status}`
3225
+ );
3226
+ return [
3227
+ "BEGIN:VEVENT",
3228
+ foldLine(`UID:${e.id}@forge0x2B`),
3229
+ `DTSTAMP:${now}`,
3230
+ `DTSTART:${dtStart}`,
3231
+ `DTEND:${dtEnd}`,
3232
+ foldLine(`SUMMARY:${summary}`),
3233
+ foldLine(`DESCRIPTION:${description}`),
3234
+ `STATUS:${e.status === "queued" ? "CONFIRMED" : "TENTATIVE"}`,
3235
+ "END:VEVENT"
3236
+ ].join("\r\n");
3237
+ });
3238
+ return [
3239
+ "BEGIN:VCALENDAR",
3240
+ "VERSION:2.0",
3241
+ "PRODID:-//forge0x2B//Scheduler//EN",
3242
+ "CALSCALE:GREGORIAN",
3243
+ "METHOD:PUBLISH",
3244
+ ...events,
3245
+ "END:VCALENDAR"
3246
+ ].join("\r\n");
3247
+ }
3248
+ function previewExport(entries, format) {
3249
+ const subset = entries.slice(0, 5);
3250
+ switch (format) {
3251
+ case "buffer":
3252
+ return exportToBufferCsv(subset);
3253
+ case "hypefury":
3254
+ return exportToHypefuryCsv(subset);
3255
+ case "icalendar":
3256
+ return exportToICalendar(subset);
3257
+ }
3258
+ }
3259
+
3260
+ exports.ANIMATION_DURATION_PRESETS = ANIMATION_DURATION_PRESETS;
3261
+ exports.BRAND_CONTENT_SLOT_KEYS = BRAND_CONTENT_SLOT_KEYS;
3262
+ exports.BUNDLED_DISPATCH_CHANNELS = BUNDLED_DISPATCH_CHANNELS;
3263
+ exports.BUNDLED_STYLE_PRESETS = BUNDLED_STYLE_PRESETS;
3264
+ exports.BUNDLED_WIDGET_TEMPLATES = BUNDLED_WIDGET_TEMPLATES;
3265
+ exports.DEFAULT_ANIMATION_DURATION_SECONDS = DEFAULT_ANIMATION_DURATION_SECONDS;
3266
+ exports.DEFAULT_BRAND_KIT_FONTS = DEFAULT_BRAND_KIT_FONTS;
3267
+ exports.DEFAULT_BRAND_KIT_PALETTE = DEFAULT_BRAND_KIT_PALETTE;
3268
+ exports.DEFAULT_BRAND_KIT_VOICE = DEFAULT_BRAND_KIT_VOICE;
3269
+ exports.DISPATCH_CHANNEL_BLOG = DISPATCH_CHANNEL_BLOG;
3270
+ exports.DISPATCH_CHANNEL_EMAIL = DISPATCH_CHANNEL_EMAIL;
3271
+ exports.DISPATCH_CHANNEL_HN = DISPATCH_CHANNEL_HN;
3272
+ exports.DISPATCH_CHANNEL_INSTAGRAM = DISPATCH_CHANNEL_INSTAGRAM;
3273
+ exports.DISPATCH_CHANNEL_LINKEDIN = DISPATCH_CHANNEL_LINKEDIN;
3274
+ exports.DISPATCH_CHANNEL_NEWSLETTER = DISPATCH_CHANNEL_NEWSLETTER;
3275
+ exports.DISPATCH_CHANNEL_REDDIT = DISPATCH_CHANNEL_REDDIT;
3276
+ exports.DISPATCH_CHANNEL_SLACK = DISPATCH_CHANNEL_SLACK;
3277
+ exports.DISPATCH_CHANNEL_TWEET = DISPATCH_CHANNEL_TWEET;
3278
+ exports.FORGE_AUDIENCES = FORGE_AUDIENCES;
3279
+ exports.FORGE_BRAND_THEME_ID = FORGE_BRAND_THEME_ID;
3280
+ exports.FORGE_TEMPLATES = FORGE_TEMPLATES;
3281
+ exports.FREE_TIER_WIDGET_IDS = FREE_TIER_WIDGET_IDS;
3282
+ exports.MAX_ANIMATION_DURATION_SECONDS = MAX_ANIMATION_DURATION_SECONDS;
3283
+ exports.MIN_ANIMATION_DURATION_SECONDS = MIN_ANIMATION_DURATION_SECONDS;
3284
+ exports.PRISM_TEMPLATES = PRISM_TEMPLATES;
3285
+ exports.PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW = PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW;
3286
+ exports.PRISM_TEMPLATE_REFACTOR_RATIONALE = PRISM_TEMPLATE_REFACTOR_RATIONALE;
3287
+ exports.PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT = PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT;
3288
+ exports.PRISM_TEMPLATE_SHIPPING_DIGEST = PRISM_TEMPLATE_SHIPPING_DIGEST;
3289
+ exports.PRISM_TEMPLATE_ZONE_DEEPDIVE = PRISM_TEMPLATE_ZONE_DEEPDIVE;
3290
+ exports.REFINE_COUNTDOWN_THRESHOLD = REFINE_COUNTDOWN_THRESHOLD;
3291
+ exports.REFINE_SESSION_SOFT_CAP = REFINE_SESSION_SOFT_CAP;
3292
+ exports.REFINE_SUGGESTIONS = REFINE_SUGGESTIONS;
3293
+ exports.STYLE_PRESET_BRUTALIST = STYLE_PRESET_BRUTALIST;
3294
+ exports.STYLE_PRESET_DEFAULT = STYLE_PRESET_DEFAULT;
3295
+ exports.STYLE_PRESET_GLASSY = STYLE_PRESET_GLASSY;
3296
+ exports.STYLE_PRESET_MINIMAL = STYLE_PRESET_MINIMAL;
3297
+ exports.TEMPLATE_SCHEMA_EXAMPLES = TEMPLATE_SCHEMA_EXAMPLES;
3298
+ exports.TEMPLATE_SCHEMA_GENERIC = TEMPLATE_SCHEMA_GENERIC;
3299
+ exports.WIDGET_TEMPLATE_CHANGELOG_ROW = WIDGET_TEMPLATE_CHANGELOG_ROW;
3300
+ exports.WIDGET_TEMPLATE_CTA_BANNER = WIDGET_TEMPLATE_CTA_BANNER;
3301
+ exports.WIDGET_TEMPLATE_FEATURE_GRID = WIDGET_TEMPLATE_FEATURE_GRID;
3302
+ exports.WIDGET_TEMPLATE_METRIC_BADGE = WIDGET_TEMPLATE_METRIC_BADGE;
3303
+ exports.WIDGET_TEMPLATE_PRICING_TIER = WIDGET_TEMPLATE_PRICING_TIER;
3304
+ exports.WIDGET_TEMPLATE_SOCIAL_PROOF = WIDGET_TEMPLATE_SOCIAL_PROOF;
3305
+ exports.WIDGET_TEMPLATE_STAT_CARD = WIDGET_TEMPLATE_STAT_CARD;
3306
+ exports.WIDGET_TEMPLATE_TESTIMONIAL = WIDGET_TEMPLATE_TESTIMONIAL;
3307
+ exports.applyEntryPatch = applyEntryPatch;
3308
+ exports.asAudienceId = asAudienceId;
3309
+ exports.assembleBrandUrlExtractionPrompt = assembleBrandUrlExtractionPrompt;
3310
+ exports.assembleForgePrompt = assembleForgePrompt;
3311
+ exports.brandThemeConfigToEntry = brandThemeConfigToEntry;
3312
+ exports.buildPrismContextPrompt = buildPrismContextPrompt;
3313
+ exports.buildRevertVersion = buildRevertVersion;
3314
+ exports.buildScheduledEntry = buildScheduledEntry;
3315
+ exports.buildVersion = buildVersion;
3316
+ exports.cascadingScheduledFor = cascadingScheduledFor;
3317
+ exports.clampAnimationDuration = clampAnimationDuration;
3318
+ exports.computeDiff = computeDiff;
3319
+ exports.defaultBrandKit = defaultBrandKit;
3320
+ exports.defaultValueForField = defaultValueForField;
3321
+ exports.deriveContextSummary = deriveContextSummary;
3322
+ exports.distill = distill;
3323
+ exports.entryInRange = entryInRange;
3324
+ exports.exportToBufferCsv = exportToBufferCsv;
3325
+ exports.exportToHypefuryCsv = exportToHypefuryCsv;
3326
+ exports.exportToICalendar = exportToICalendar;
3327
+ exports.extractBrandFromPrismBlueprint = extractBrandFromPrismBlueprint;
3328
+ exports.extractDependencyHotspots = extractDependencyHotspots;
3329
+ exports.extractTopChurnFiles = extractTopChurnFiles;
3330
+ exports.extractZones = extractZones;
3331
+ exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
3332
+ exports.generateAskDrivenAsset = generateAskDrivenAsset;
3333
+ exports.generateChangesSince = generateChangesSince;
3334
+ exports.generateOnboardingDoc = generateOnboardingDoc;
3335
+ exports.generateRefactoringReport = generateRefactoringReport;
3336
+ exports.generateReleaseNotes = generateReleaseNotes;
3337
+ exports.getDispatchChannel = getDispatchChannel;
3338
+ exports.getPrismTemplate = getPrismTemplate;
3339
+ exports.getSlotValue = getSlotValue;
3340
+ exports.getStyleById = getStyleById;
3341
+ exports.getWidgetTemplate = getWidgetTemplate;
3342
+ exports.initialFormValues = initialFormValues;
3343
+ exports.next7DaysRange = next7DaysRange;
3344
+ exports.nextVersionNumber = nextVersionNumber;
3345
+ exports.orchestrateDispatch = orchestrateDispatch;
3346
+ exports.parseAndValidateSchemaDefinition = parseAndValidateSchemaDefinition;
3347
+ exports.parseBrandKitFromLlmResponse = parseBrandKitFromLlmResponse;
3348
+ exports.parseBrandThemeContent = parseBrandThemeContent;
3349
+ exports.parseStyleFromCss = parseStyleFromCss;
3350
+ exports.parseStyleFromTailwindConfig = parseStyleFromTailwindConfig;
3351
+ exports.parseStyleFromTokensJson = parseStyleFromTokensJson;
3352
+ exports.parseThemeConfigContent = parseThemeConfigContent;
3353
+ exports.previewExport = previewExport;
3354
+ exports.readBlueprintData = readBlueprintData;
3355
+ exports.readBlueprintFromTarget = readBlueprintFromTarget;
3356
+ exports.readPrismDirectory = readPrismDirectory;
3357
+ exports.refineAsset = refineAsset;
3358
+ exports.refineLimitState = refineLimitState;
3359
+ exports.renderWidget = renderWidget;
3360
+ exports.resolveAnimatedChoice = resolveAnimatedChoice;
3361
+ exports.resolveAnimationDuration = resolveAnimationDuration;
3362
+ exports.resolveBrandPalette = resolveBrandPalette;
3363
+ exports.runForgeGeneration = runForgeGeneration;
3364
+ exports.scanForSecrets = scanForSecrets;
3365
+ exports.schemaExampleFor = schemaExampleFor;
3366
+ exports.schemaToForm = schemaToForm;
3367
+ exports.templateAnimatedDefault = templateAnimatedDefault;
3368
+ exports.themeEntryToPalette = themeEntryToPalette;
3369
+ exports.tryParseJsonObject = tryParseJsonObject;
3370
+ exports.validateAgainstTemplateSchema = validateAgainstTemplateSchema;
3371
+ exports.validateAssetSlots = validateAssetSlots;
3372
+ exports.validateFormValues = validateFormValues;
3373
+ exports.validateSchemaDefinition = validateSchemaDefinition;