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.
package/dist/index.cjs CHANGED
@@ -1,12 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var fs = require('fs/promises');
4
- var path = require('path');
5
-
6
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
-
8
- var fs__default = /*#__PURE__*/_interopDefault(fs);
9
- var path__default = /*#__PURE__*/_interopDefault(path);
3
+ var crypto = require('crypto');
10
4
 
11
5
  // src/generators/releaseNotes.ts
12
6
  function buildSystemPrompt() {
@@ -277,12 +271,12 @@ function detectImportCycles(edges) {
277
271
  const cycles = [];
278
272
  const visited = /* @__PURE__ */ new Set();
279
273
  const stack = /* @__PURE__ */ new Set();
280
- function dfs(node, path2) {
274
+ function dfs(node, path) {
281
275
  if (cycles.length >= 5) return;
282
276
  if (stack.has(node)) {
283
- const cycleStart = path2.indexOf(node);
277
+ const cycleStart = path.indexOf(node);
284
278
  if (cycleStart !== -1) {
285
- cycles.push(path2.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
279
+ cycles.push(path.slice(cycleStart).join(" \u2192 ") + " \u2192 " + node);
286
280
  }
287
281
  return;
288
282
  }
@@ -290,7 +284,7 @@ function detectImportCycles(edges) {
290
284
  visited.add(node);
291
285
  stack.add(node);
292
286
  for (const neighbor of graph.get(node) ?? []) {
293
- dfs(neighbor, [...path2, node]);
287
+ dfs(neighbor, [...path, node]);
294
288
  }
295
289
  stack.delete(node);
296
290
  }
@@ -345,43 +339,6 @@ async function generateRefactoringReport(blueprint, opts, provider) {
345
339
  metadata: { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), usedTokens: response.usedTokens, generator: "forgesmith" }
346
340
  };
347
341
  }
348
- async function readJsonFiles(dir) {
349
- try {
350
- const entries = await fs__default.default.readdir(dir);
351
- const results = [];
352
- for (const entry of entries) {
353
- if (!entry.endsWith(".json")) continue;
354
- try {
355
- const raw = await fs__default.default.readFile(path__default.default.join(dir, entry), "utf-8");
356
- results.push(JSON.parse(raw));
357
- } catch {
358
- }
359
- }
360
- return results;
361
- } catch {
362
- return [];
363
- }
364
- }
365
- async function readPrismDirectory(prismPath) {
366
- const sessionsDir = path__default.default.join(prismPath, "sessions");
367
- const recsDir = path__default.default.join(prismPath, "recommendations");
368
- const insightsDir = path__default.default.join(prismPath, "green", "insights", "accepted");
369
- const [sessions, recommendations, insights] = await Promise.all([
370
- readJsonFiles(sessionsDir),
371
- readJsonFiles(recsDir),
372
- readJsonFiles(insightsDir)
373
- ]);
374
- return { sessions, recommendations, insights };
375
- }
376
- async function readBlueprintData(targetPath) {
377
- const snapshotPath = path__default.default.join(targetPath, ".prism", "blueprint", "snapshot.json");
378
- try {
379
- const raw = await fs__default.default.readFile(snapshotPath, "utf-8");
380
- return JSON.parse(raw);
381
- } catch {
382
- return null;
383
- }
384
- }
385
342
 
386
343
  // src/generators/askDrivenAsset.ts
387
344
  var NO_DATA_MSG5 = "No Blueprint data available. Run prism scan first.";
@@ -465,11 +422,2641 @@ async function generateAskDrivenAsset(blueprint, question, opts, provider) {
465
422
  };
466
423
  }
467
424
 
425
+ // src/forge/types.ts
426
+ var asAudienceId = (id) => id;
427
+
428
+ // src/forge/distill.ts
429
+ var MIN_CLAIMS = 3;
430
+ var MAX_CLAIMS = 6;
431
+ function clampClaims(claims) {
432
+ if (claims.length <= MAX_CLAIMS) return claims;
433
+ return claims.slice(0, MAX_CLAIMS);
434
+ }
435
+ function splitSentences(text) {
436
+ return text.split(/(?<=[.!?])\s+/).map((s) => s.trim()).filter((s) => s.length > 0);
437
+ }
438
+ function fromBrief(payload) {
439
+ const body = typeof payload.body === "string" ? payload.body : "";
440
+ return splitSentences(body).slice(0, MAX_CLAIMS);
441
+ }
442
+ function fromKnowledgeRef(payload) {
443
+ const ids = Array.isArray(payload.blockIds) ? payload.blockIds : [];
444
+ const note = typeof payload.note === "string" ? payload.note : "";
445
+ const out = [];
446
+ if (note.trim().length > 0) out.push(note.trim());
447
+ for (const id of ids) {
448
+ out.push(`Knowledge reference: ${id}`);
449
+ if (out.length >= MAX_CLAIMS) break;
450
+ }
451
+ return out;
452
+ }
453
+ function fromGitRange(payload) {
454
+ const summary = payload.summary ?? {};
455
+ const commits = Array.isArray(summary.commits) ? summary.commits : [];
456
+ const subjects = commits.map((c) => typeof c?.subject === "string" ? c.subject : "").filter((s) => s.length > 0);
457
+ const files = typeof summary.files_changed_summary === "string" ? summary.files_changed_summary.split("\n").filter((l) => l.trim().length > 0) : [];
458
+ const out = [];
459
+ if (subjects.length > 0) {
460
+ out.push(`${commits.length} commit(s): ${subjects.slice(0, 3).join("; ")}`);
461
+ }
462
+ if (files.length > 0) {
463
+ const tail = files[files.length - 1] ?? "";
464
+ out.push(`Changes: ${tail.trim()}`);
465
+ }
466
+ for (const subj of subjects.slice(1, MAX_CLAIMS)) {
467
+ out.push(subj);
468
+ if (out.length >= MAX_CLAIMS) break;
469
+ }
470
+ return out;
471
+ }
472
+ function fromAppFeatureRef(payload) {
473
+ const ids = Array.isArray(payload.featureIds) ? payload.featureIds : [];
474
+ const note = typeof payload.note === "string" ? payload.note : "";
475
+ const out = [];
476
+ if (note.trim().length > 0) out.push(note.trim());
477
+ out.push(`References ${ids.length} app feature(s): ${ids.slice(0, 5).join(", ")}`);
478
+ return out;
479
+ }
480
+ function fromBusinessCapabilityRef(payload) {
481
+ const capabilityName = typeof payload.capability_name === "string" ? payload.capability_name : "";
482
+ const resolvedClaims = Array.isArray(payload.resolvedClaims) ? payload.resolvedClaims : [];
483
+ const out = [];
484
+ if (capabilityName.trim().length > 0) {
485
+ out.push(`Business capability: ${capabilityName}`);
486
+ }
487
+ for (const claim of resolvedClaims) {
488
+ if (typeof claim === "string" && claim.trim().length > 0) {
489
+ out.push(claim.trim());
490
+ if (out.length >= MAX_CLAIMS) break;
491
+ }
492
+ }
493
+ return out;
494
+ }
495
+ function buildSourceRefs(signal) {
496
+ const refs = signal.source_refs ?? {};
497
+ const collected = [];
498
+ for (const [key, val] of Object.entries(refs)) {
499
+ if (Array.isArray(val)) {
500
+ for (const item of val) {
501
+ if (typeof item === "string") collected.push(`${key}:${item}`);
502
+ else collected.push(`${key}:${JSON.stringify(item)}`);
503
+ }
504
+ }
505
+ }
506
+ if (collected.length === 0) collected.push(`signal:${signal.id}`);
507
+ return collected;
508
+ }
509
+ function distill(input) {
510
+ const { signal, audience } = input;
511
+ const payload = signal.payload ?? {};
512
+ let statements = [];
513
+ switch (signal.kind) {
514
+ case "brief":
515
+ statements = fromBrief(payload);
516
+ break;
517
+ case "knowledge_ref":
518
+ statements = fromKnowledgeRef(payload);
519
+ break;
520
+ case "git_range":
521
+ statements = fromGitRange(payload);
522
+ break;
523
+ case "app_feature_ref":
524
+ statements = fromAppFeatureRef(payload);
525
+ break;
526
+ case "business_capability_ref":
527
+ statements = fromBusinessCapabilityRef(payload);
528
+ break;
529
+ // amber_capability_ref uses the same pre-resolved payload shape as business_capability_ref.
530
+ case "amber_capability_ref":
531
+ statements = fromBusinessCapabilityRef(payload);
532
+ break;
533
+ // platform_walkthrough is reserved for the platform-adapter; graceful no-op here.
534
+ case "platform_walkthrough":
535
+ statements = [];
536
+ break;
537
+ // green_insight_ref: platform-adapter resolves insight_title + resolvedClaims before distill.
538
+ case "green_insight_ref": {
539
+ const insightTitle = typeof payload.insight_title === "string" ? payload.insight_title : "";
540
+ const resolvedClaims = Array.isArray(payload.resolvedClaims) ? payload.resolvedClaims : [];
541
+ const out = [];
542
+ if (insightTitle.trim().length > 0) {
543
+ out.push(`GREEN insight: ${insightTitle}`);
544
+ }
545
+ for (const c of resolvedClaims) {
546
+ if (typeof c === "string" && c.trim().length > 0) {
547
+ out.push(c.trim());
548
+ }
549
+ }
550
+ statements = out;
551
+ break;
552
+ }
553
+ }
554
+ if (statements.length < MIN_CLAIMS) {
555
+ const fallback = `Signal of kind ${signal.kind} for audience ${audience.id}.`;
556
+ while (statements.length < MIN_CLAIMS) statements.push(fallback);
557
+ }
558
+ const refs = buildSourceRefs(signal);
559
+ const claims = clampClaims(
560
+ statements.map((statement, i) => ({
561
+ id: `c${i + 1}`,
562
+ statement,
563
+ source_ref: refs[i % refs.length] ?? `signal:${signal.id}`
564
+ }))
565
+ );
566
+ return {
567
+ signal_id: signal.id,
568
+ audience_id: String(audience.id),
569
+ audience_label: audience.label,
570
+ tonality: audience.tonality,
571
+ reading_level: audience.reading_level,
572
+ claims,
573
+ raw_source_refs: signal.source_refs ?? {}
574
+ };
575
+ }
576
+
577
+ // src/forge/promptAssembly.ts
578
+ var EXECUTOR_SPICKZETTEL = `You are a precise execution agent. Your role is to carry out a specific step given a fully-specified context.
579
+ - You act on the provided state and knowledge blocks.
580
+ - You return a concrete next_action and state_delta.
581
+ - You never invent facts. If domain rules are missing, you set knowledge_missing.
582
+ - Confidence reflects how certain you are that your action is correct.`;
583
+ var FORGE_WRITER_SPICKZETTEL = `${EXECUTOR_SPICKZETTEL}
584
+
585
+ You write product communication. Your job is to take a structured ProductTruth
586
+ (claims + source refs) and produce a JSON object that fills the template's
587
+ schema_json. Stay inside the audience's tonality and reading level. Never
588
+ invent facts that are not present in the ProductTruth.`;
589
+ function appendJsonSchemaInstruction(systemPrompt, schemaName, schema) {
590
+ const schemaText = JSON.stringify(schema, null, 2);
591
+ const block = [
592
+ "",
593
+ "\u2500\u2500\u2500 OUTPUT CONTRACT \u2500\u2500\u2500",
594
+ `You MUST respond with a single valid JSON object that satisfies the schema named "${schemaName}".`,
595
+ "Return ONLY the JSON object. No prose before or after. No markdown fences.",
596
+ "If a field cannot be filled given the inputs, set it to an empty string or empty array \u2014 do NOT omit required keys.",
597
+ "",
598
+ `Schema (${schemaName}):`,
599
+ schemaText
600
+ ].join("\n");
601
+ return `${systemPrompt}
602
+ ${block}`;
603
+ }
604
+ function tonalityInstruction(audience) {
605
+ return [
606
+ `Audience: ${audience.label} (${audience.id}).`,
607
+ `Tonality: ${audience.tonality}.`,
608
+ `Reading level: ${audience.reading_level}.`,
609
+ `Channel hints: ${audience.channel_hints.join(", ")}.`,
610
+ audience.description
611
+ ].join(" ");
612
+ }
613
+ function formatSpec(resolved) {
614
+ return [
615
+ `Channel: ${resolved.channel}.`,
616
+ `Intent: ${resolved.intent}.`,
617
+ `Format: ${resolved.format}.`,
618
+ `Modality: ${resolved.modality}.`
619
+ ].join(" ");
620
+ }
621
+ function interactiveVisualSlotsBlock(template) {
622
+ const slots = Array.isArray(template.asset_slots) ? template.asset_slots : [];
623
+ const visual = slots.map(
624
+ (raw) => typeof raw === "object" && raw !== null ? raw : null
625
+ ).filter(
626
+ (s) => s !== null && s.modality === "interactive_visual"
627
+ );
628
+ if (visual.length === 0) return null;
629
+ const lines = ["\u2500\u2500\u2500 INTERACTIVE VISUAL SLOTS \u2500\u2500\u2500"];
630
+ const perDocument = [];
631
+ const perSlide = [];
632
+ visual.forEach((slot, i) => {
633
+ const kind = typeof slot.kind === "string" ? slot.kind : `slot_${i}`;
634
+ const per = typeof slot.per === "string" ? slot.per : "document";
635
+ const rs = slot.render_spec ?? {};
636
+ const w = typeof rs.width === "number" ? rs.width : 800;
637
+ const h = typeof rs.height === "number" ? rs.height : 450;
638
+ const ar = typeof rs.aspect_ratio === "string" ? rs.aspect_ratio : `${w}:${h}`;
639
+ lines.push(
640
+ `\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.`
641
+ );
642
+ if (per === "slide") perSlide.push(slot);
643
+ else perDocument.push(slot);
644
+ });
645
+ lines.push("");
646
+ lines.push("REQUIRED OUTPUT FIELDS for the interactive_visual slots above:");
647
+ if (perDocument.length > 0) {
648
+ lines.push(
649
+ "\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."
650
+ );
651
+ }
652
+ if (perSlide.length > 0) {
653
+ lines.push(
654
+ "\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."
655
+ );
656
+ }
657
+ lines.push(
658
+ "These fields are in addition to the schema's other required keys; do NOT omit them. Without them the visual cannot be rendered."
659
+ );
660
+ return lines.join("\n");
661
+ }
662
+ function productTruthBlock(truth) {
663
+ const claims = truth.claims.map((c) => `- (${c.id}) [${c.source_ref}] ${c.statement}`).join("\n");
664
+ return ["ProductTruth (deterministic, do not extend):", claims].join("\n");
665
+ }
666
+ function assembleForgePrompt(input) {
667
+ const { template, audience, productTruth, resolved, contextBlocks } = input;
668
+ const visualBlock = interactiveVisualSlotsBlock(template);
669
+ const systemBase = [
670
+ FORGE_WRITER_SPICKZETTEL,
671
+ "",
672
+ "\u2500\u2500\u2500 AUDIENCE \u2500\u2500\u2500",
673
+ tonalityInstruction(audience),
674
+ "",
675
+ "\u2500\u2500\u2500 FORMAT \u2500\u2500\u2500",
676
+ formatSpec(resolved),
677
+ ...visualBlock ? ["", visualBlock] : []
678
+ ].join("\n");
679
+ const schemaName = `forge.${template.id}`;
680
+ const system = appendJsonSchemaInstruction(systemBase, schemaName, template.schema_json);
681
+ const blocks = (contextBlocks ?? []).filter(
682
+ (b) => typeof b === "string" && b.trim().length > 0
683
+ );
684
+ const userParts = [];
685
+ for (const b of blocks) {
686
+ userParts.push(b);
687
+ userParts.push("");
688
+ }
689
+ userParts.push(productTruthBlock(productTruth));
690
+ userParts.push("");
691
+ userParts.push("Produce the JSON object now.");
692
+ const user = userParts.join("\n");
693
+ return { system, user, schema_name: schemaName, schema: template.schema_json };
694
+ }
695
+
696
+ // src/forge/validateResult.ts
697
+ function isStringValue(v) {
698
+ return typeof v === "string" && v.trim().length > 0;
699
+ }
700
+ function isPlainObject(v) {
701
+ return typeof v === "object" && v !== null && !Array.isArray(v);
702
+ }
703
+ function isSchemaSections(s) {
704
+ return isPlainObject(s) && Array.isArray(s.sections) && s.sections.every((x) => typeof x === "string");
705
+ }
706
+ function isSchemaSlides(s) {
707
+ if (!isPlainObject(s)) return false;
708
+ const sl = s.slides;
709
+ return isPlainObject(sl) && typeof sl.min === "number" && typeof sl.max === "number";
710
+ }
711
+ function validateAgainstTemplateSchema(schema, candidate) {
712
+ if (!isPlainObject(candidate)) {
713
+ return { ok: false, errors: ["result is not a JSON object"] };
714
+ }
715
+ if (isSchemaSections(schema)) {
716
+ const missing = [];
717
+ for (const key of schema.sections) {
718
+ if (!isStringValue(candidate[key])) missing.push(key);
719
+ }
720
+ if (missing.length > 0) {
721
+ return {
722
+ ok: false,
723
+ errors: [`missing or empty section field(s): ${missing.join(", ")}`]
724
+ };
725
+ }
726
+ return { ok: true, value: candidate };
727
+ }
728
+ if (isSchemaSlides(schema)) {
729
+ const slides = candidate.slides;
730
+ if (!Array.isArray(slides)) {
731
+ return { ok: false, errors: ["slides must be an array"] };
732
+ }
733
+ const { min, max } = schema.slides;
734
+ if (slides.length < min || slides.length > max) {
735
+ return {
736
+ ok: false,
737
+ errors: [`slides length ${slides.length} not in [${min}, ${max}]`]
738
+ };
739
+ }
740
+ const slideErrors = [];
741
+ slides.forEach((s, i) => {
742
+ if (!isPlainObject(s)) {
743
+ slideErrors.push(`slide[${i}] not an object`);
744
+ return;
745
+ }
746
+ const hasBody = isStringValue(s.body);
747
+ const hasHeadline = isStringValue(s.headline);
748
+ if (!hasBody && !hasHeadline) {
749
+ slideErrors.push(`slide[${i}] requires non-empty body or headline`);
750
+ }
751
+ });
752
+ if (slideErrors.length > 0) {
753
+ return { ok: false, errors: slideErrors };
754
+ }
755
+ return { ok: true, value: candidate };
756
+ }
757
+ return {
758
+ ok: false,
759
+ errors: ["template schema_json is not in a supported shape"],
760
+ unsupported: ["unrecognized schema shape"]
761
+ };
762
+ }
763
+ function tryParseJsonObject(raw) {
764
+ let s = raw.trim();
765
+ const fenceMatch = s.match(/```(?:json)?\s*([\s\S]*?)```/i);
766
+ if (fenceMatch) s = fenceMatch[1].trim();
767
+ const first = s.indexOf("{");
768
+ const last = s.lastIndexOf("}");
769
+ if (first === -1 || last === -1 || last <= first) {
770
+ throw new Error("no JSON object found in model output");
771
+ }
772
+ const slice = s.slice(first, last + 1);
773
+ return JSON.parse(slice);
774
+ }
775
+
776
+ // src/forge/validateSchemaDefinition.ts
777
+ function isPlainObject2(v) {
778
+ return typeof v === "object" && v !== null && !Array.isArray(v);
779
+ }
780
+ function validateSchemaDefinition(input) {
781
+ if (!isPlainObject2(input)) {
782
+ return { valid: false, errors: ["schema_json must be an object"] };
783
+ }
784
+ if (typeof input.type !== "string" || input.type.length === 0) {
785
+ return {
786
+ valid: false,
787
+ errors: ["schema_json must declare a top-level `type` (e.g. 'object')"]
788
+ };
789
+ }
790
+ if (input.type === "object" && input.properties !== void 0) {
791
+ if (!isPlainObject2(input.properties)) {
792
+ return {
793
+ valid: false,
794
+ errors: ["schema_json.properties must be a flat object record"]
795
+ };
796
+ }
797
+ const propErrors = [];
798
+ for (const [name, def] of Object.entries(input.properties)) {
799
+ if (!isPlainObject2(def)) {
800
+ propErrors.push(`properties.${name} must be an object`);
801
+ continue;
802
+ }
803
+ if (typeof def.type !== "string" || def.type.length === 0) {
804
+ propErrors.push(`properties.${name}.type required`);
805
+ }
806
+ }
807
+ if (propErrors.length > 0) {
808
+ return { valid: false, errors: propErrors };
809
+ }
810
+ }
811
+ return { valid: true };
812
+ }
813
+ function parseAndValidateSchemaDefinition(jsonText) {
814
+ let parsed;
815
+ try {
816
+ parsed = JSON.parse(jsonText);
817
+ } catch (e) {
818
+ return {
819
+ valid: false,
820
+ errors: [
821
+ `schema_json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`
822
+ ]
823
+ };
824
+ }
825
+ const result = validateSchemaDefinition(parsed);
826
+ if (!result.valid) return result;
827
+ return { valid: true, parsed };
828
+ }
829
+
830
+ // src/forge/validateAssetSlots.ts
831
+ var VALID_MODALITIES = [
832
+ "text",
833
+ "text_with_visual_brief",
834
+ "interactive_visual",
835
+ "image",
836
+ "video",
837
+ "mixed"
838
+ ];
839
+ var VALID_PER = /* @__PURE__ */ new Set(["document", "slide"]);
840
+ var ASPECT_RATIO_RE = /^\d+:\d+$/;
841
+ var MIN_W = 320;
842
+ var MAX_W = 4096;
843
+ var MIN_H = 240;
844
+ var MAX_H = 4096;
845
+ function isPlainObject3(v) {
846
+ return typeof v === "object" && v !== null && !Array.isArray(v);
847
+ }
848
+ function isInt(v) {
849
+ return typeof v === "number" && Number.isFinite(v) && Number.isInteger(v);
850
+ }
851
+ function validateAssetSlots(input) {
852
+ if (!Array.isArray(input)) {
853
+ return { valid: false, errors: ["asset_slots must be an array"] };
854
+ }
855
+ const errors = [];
856
+ input.forEach((raw, i) => {
857
+ if (!isPlainObject3(raw)) {
858
+ errors.push(`asset_slots[${i}] must be an object`);
859
+ return;
860
+ }
861
+ const kind = raw.kind;
862
+ if (typeof kind !== "string" || kind.length === 0) {
863
+ errors.push(`asset_slots[${i}].kind must be a non-empty string`);
864
+ }
865
+ const per = raw.per;
866
+ if (typeof per !== "string" || !VALID_PER.has(per)) {
867
+ errors.push(
868
+ `asset_slots[${i}].per must be 'document' or 'slide' (got ${JSON.stringify(per)})`
869
+ );
870
+ }
871
+ const modality = raw.modality;
872
+ if (typeof modality !== "string" || !VALID_MODALITIES.includes(modality)) {
873
+ errors.push(
874
+ `asset_slots[${i}].modality invalid (got ${JSON.stringify(modality)})`
875
+ );
876
+ }
877
+ if (modality === "interactive_visual") {
878
+ const rs = raw.render_spec;
879
+ if (!isPlainObject3(rs)) {
880
+ errors.push(
881
+ `asset_slots[${i}].render_spec required when modality='interactive_visual'`
882
+ );
883
+ } else {
884
+ const ar = rs.aspect_ratio;
885
+ if (typeof ar !== "string" || !ASPECT_RATIO_RE.test(ar)) {
886
+ errors.push(
887
+ `asset_slots[${i}].render_spec.aspect_ratio must match \\d+:\\d+`
888
+ );
889
+ }
890
+ const w = rs.width;
891
+ if (!isInt(w) || w < MIN_W || w > MAX_W) {
892
+ errors.push(
893
+ `asset_slots[${i}].render_spec.width must be an integer in [${MIN_W}, ${MAX_W}]`
894
+ );
895
+ }
896
+ const h = rs.height;
897
+ if (!isInt(h) || h < MIN_H || h > MAX_H) {
898
+ errors.push(
899
+ `asset_slots[${i}].render_spec.height must be an integer in [${MIN_H}, ${MAX_H}]`
900
+ );
901
+ }
902
+ if (rs.animated !== void 0 && typeof rs.animated !== "boolean") {
903
+ errors.push(
904
+ `asset_slots[${i}].render_spec.animated must be a boolean when present`
905
+ );
906
+ }
907
+ }
908
+ }
909
+ });
910
+ return errors.length === 0 ? { valid: true } : { valid: false, errors };
911
+ }
912
+
913
+ // src/forge/animationChoice.ts
914
+ function isPlainObject4(v) {
915
+ return typeof v === "object" && v !== null && !Array.isArray(v);
916
+ }
917
+ function templateAnimatedDefault(assetSlots) {
918
+ if (!Array.isArray(assetSlots)) return false;
919
+ for (const raw of assetSlots) {
920
+ if (!isPlainObject4(raw)) continue;
921
+ if (raw.modality !== "interactive_visual") continue;
922
+ const rs = raw.render_spec;
923
+ if (isPlainObject4(rs) && rs.animated === true) return true;
924
+ }
925
+ return false;
926
+ }
927
+ function resolveAnimatedChoice(templateDefault, override) {
928
+ return typeof override === "boolean" ? override : templateDefault;
929
+ }
930
+
931
+ // src/forge/animationDuration.ts
932
+ var DEFAULT_ANIMATION_DURATION_SECONDS = 10;
933
+ var ANIMATION_DURATION_PRESETS = [5, 10, 20, 30];
934
+ var MIN_ANIMATION_DURATION_SECONDS = 1;
935
+ var MAX_ANIMATION_DURATION_SECONDS = 120;
936
+ function isUsableDuration(v) {
937
+ return typeof v === "number" && Number.isFinite(v) && v > 0;
938
+ }
939
+ function clampAnimationDuration(seconds) {
940
+ if (!Number.isFinite(seconds)) return DEFAULT_ANIMATION_DURATION_SECONDS;
941
+ return Math.min(
942
+ MAX_ANIMATION_DURATION_SECONDS,
943
+ Math.max(MIN_ANIMATION_DURATION_SECONDS, Math.round(seconds))
944
+ );
945
+ }
946
+ function resolveAnimationDuration(override, persisted, fallback = DEFAULT_ANIMATION_DURATION_SECONDS) {
947
+ if (isUsableDuration(override)) return clampAnimationDuration(override);
948
+ if (isUsableDuration(persisted)) return clampAnimationDuration(persisted);
949
+ return clampAnimationDuration(fallback);
950
+ }
951
+
952
+ // src/forge/refineLimit.ts
953
+ var REFINE_SESSION_SOFT_CAP = 100;
954
+ var REFINE_COUNTDOWN_THRESHOLD = 90;
955
+ function refineLimitState(usedRefines, softCap = REFINE_SESSION_SOFT_CAP) {
956
+ const used = Number.isFinite(usedRefines) && usedRefines > 0 ? Math.floor(usedRefines) : 0;
957
+ const remaining = Math.max(0, softCap - used);
958
+ return {
959
+ used,
960
+ capped: used >= softCap,
961
+ remaining,
962
+ showCountdown: used >= REFINE_COUNTDOWN_THRESHOLD
963
+ };
964
+ }
965
+
966
+ // src/forge/brandKit.ts
967
+ var DEFAULT_BRAND_KIT_PALETTE = {
968
+ primary: "#f97316",
969
+ secondary: "#1e293b",
970
+ accent: "#fb923c",
971
+ surface: "#0f172a",
972
+ text: "#f1f5f9",
973
+ muted: "#64748b"
974
+ };
975
+ var DEFAULT_BRAND_KIT_FONTS = {
976
+ heading: "Inter",
977
+ body: "Inter",
978
+ mono: "JetBrains Mono"
979
+ };
980
+ var DEFAULT_BRAND_KIT_VOICE = {
981
+ tone: "professional",
982
+ audience: "developers",
983
+ vocabulary: [],
984
+ avoid: [],
985
+ formality: 60,
986
+ technicality: 70
987
+ };
988
+ function defaultBrandKit(partial) {
989
+ return {
990
+ name: "My Brand",
991
+ source: "manual",
992
+ palette: { ...DEFAULT_BRAND_KIT_PALETTE },
993
+ fonts: { ...DEFAULT_BRAND_KIT_FONTS },
994
+ voice: { ...DEFAULT_BRAND_KIT_VOICE },
995
+ logo: {},
996
+ version: 1,
997
+ versions: [],
998
+ ...partial
999
+ };
1000
+ }
1001
+ function assembleBrandUrlExtractionPrompt(hints) {
1002
+ const lines = [
1003
+ `Extract a brand kit from the following website metadata.`,
1004
+ `URL: ${hints.url}`
1005
+ ];
1006
+ if (hints.pageTitle) lines.push(`Page title: ${hints.pageTitle}`);
1007
+ if (hints.metaDescription) lines.push(`Meta description: ${hints.metaDescription}`);
1008
+ if (hints.themeColor) lines.push(`Theme color: ${hints.themeColor}`);
1009
+ if (hints.cssColorHints?.length) lines.push(`Color hints from CSS/HTML: ${hints.cssColorHints.join(", ")}`);
1010
+ if (hints.fontHints?.length) lines.push(`Font hints: ${hints.fontHints.join(", ")}`);
1011
+ lines.push(
1012
+ "",
1013
+ "Return a JSON object with exactly this structure (use hex colors, fill all fields with your best inference):",
1014
+ JSON.stringify({
1015
+ name: "<brand name>",
1016
+ palette: {
1017
+ primary: "#rrggbb",
1018
+ secondary: "#rrggbb",
1019
+ accent: "#rrggbb",
1020
+ surface: "#rrggbb",
1021
+ text: "#rrggbb",
1022
+ muted: "#rrggbb"
1023
+ },
1024
+ fonts: {
1025
+ heading: "<font name>",
1026
+ body: "<font name>",
1027
+ mono: "<font name or 'monospace'>"
1028
+ },
1029
+ voice: {
1030
+ tone: "<professional|technical|casual|inspirational|authoritative>",
1031
+ audience: "<developers|executives|general|designers|business>",
1032
+ vocabulary: ["<preferred term>"],
1033
+ avoid: ["<term to avoid>"],
1034
+ formality: 70,
1035
+ technicality: 50
1036
+ },
1037
+ logo: { url: "<logo url if known or empty string>" }
1038
+ }, null, 2),
1039
+ "",
1040
+ "Respond with ONLY the JSON object, no markdown fences, no explanation."
1041
+ );
1042
+ return lines.join("\n");
1043
+ }
1044
+ function isValidHex(v) {
1045
+ return typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v.trim());
1046
+ }
1047
+ function safeHex(v, fallback) {
1048
+ return isValidHex(v) ? v.trim() : fallback;
1049
+ }
1050
+ function safeStr(v, fallback) {
1051
+ return typeof v === "string" && v.trim() ? v.trim() : fallback;
1052
+ }
1053
+ function safeNum(v, fallback) {
1054
+ const n = Number(v);
1055
+ return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : fallback;
1056
+ }
1057
+ function parseBrandKitFromLlmResponse(text, hints) {
1058
+ let raw;
1059
+ const cleaned = text.replace(/^```json\s*/i, "").replace(/^```\s*/i, "").replace(/\s*```\s*$/i, "").trim();
1060
+ try {
1061
+ raw = JSON.parse(cleaned);
1062
+ } catch {
1063
+ const match = /\{[\s\S]*\}/.exec(text);
1064
+ if (match) {
1065
+ try {
1066
+ raw = JSON.parse(match[0]);
1067
+ } catch {
1068
+ raw = {};
1069
+ }
1070
+ } else {
1071
+ raw = {};
1072
+ }
1073
+ }
1074
+ const r = raw && typeof raw === "object" ? raw : {};
1075
+ const palette = r.palette && typeof r.palette === "object" ? r.palette : {};
1076
+ const fonts = r.fonts && typeof r.fonts === "object" ? r.fonts : {};
1077
+ const voice = r.voice && typeof r.voice === "object" ? r.voice : {};
1078
+ const logo = r.logo && typeof r.logo === "object" ? r.logo : {};
1079
+ const vocab = Array.isArray(voice.vocabulary) ? voice.vocabulary.filter((v) => typeof v === "string") : [];
1080
+ const avoid = Array.isArray(voice.avoid) ? voice.avoid.filter((v) => typeof v === "string") : [];
1081
+ return {
1082
+ name: safeStr(r.name, new URL(hints.url).hostname.replace(/^www\./, "")),
1083
+ source: "url",
1084
+ source_url: hints.url,
1085
+ palette: {
1086
+ primary: safeHex(palette.primary, hints.themeColor ?? DEFAULT_BRAND_KIT_PALETTE.primary),
1087
+ secondary: safeHex(palette.secondary, DEFAULT_BRAND_KIT_PALETTE.secondary),
1088
+ accent: safeHex(palette.accent, DEFAULT_BRAND_KIT_PALETTE.accent),
1089
+ surface: safeHex(palette.surface, DEFAULT_BRAND_KIT_PALETTE.surface),
1090
+ text: safeHex(palette.text, DEFAULT_BRAND_KIT_PALETTE.text),
1091
+ muted: safeHex(palette.muted, DEFAULT_BRAND_KIT_PALETTE.muted)
1092
+ },
1093
+ fonts: {
1094
+ heading: safeStr(fonts.heading, DEFAULT_BRAND_KIT_FONTS.heading),
1095
+ body: safeStr(fonts.body, DEFAULT_BRAND_KIT_FONTS.body),
1096
+ mono: safeStr(fonts.mono, DEFAULT_BRAND_KIT_FONTS.mono)
1097
+ },
1098
+ voice: {
1099
+ tone: safeStr(voice.tone, DEFAULT_BRAND_KIT_VOICE.tone),
1100
+ audience: safeStr(voice.audience, DEFAULT_BRAND_KIT_VOICE.audience),
1101
+ vocabulary: vocab,
1102
+ avoid,
1103
+ formality: safeNum(voice.formality, DEFAULT_BRAND_KIT_VOICE.formality),
1104
+ technicality: safeNum(voice.technicality, DEFAULT_BRAND_KIT_VOICE.technicality)
1105
+ },
1106
+ logo: { url: safeStr(logo.url, "") || void 0 },
1107
+ prism_detected: false
1108
+ };
1109
+ }
1110
+
1111
+ // src/forge/brandPalette.ts
1112
+ function isNonEmptyString(v) {
1113
+ return typeof v === "string" && v.trim().length > 0;
1114
+ }
1115
+ function themeEntryToPalette(entry) {
1116
+ if (!entry || typeof entry !== "object") return null;
1117
+ const { primary, accent, bg, text } = entry;
1118
+ if (!isNonEmptyString(primary) || !isNonEmptyString(accent) || !isNonEmptyString(bg) || !isNonEmptyString(text)) {
1119
+ return null;
1120
+ }
1121
+ return {
1122
+ primary: primary.trim(),
1123
+ accent: accent.trim(),
1124
+ background: bg.trim(),
1125
+ card: isNonEmptyString(entry.card) ? entry.card.trim() : "#ffffff",
1126
+ text: text.trim()
1127
+ };
1128
+ }
1129
+ function parseThemeConfigContent(content) {
1130
+ let value = content;
1131
+ if (typeof value === "string") {
1132
+ try {
1133
+ value = JSON.parse(value);
1134
+ } catch {
1135
+ return [];
1136
+ }
1137
+ }
1138
+ if (!Array.isArray(value)) return [];
1139
+ return value.filter(
1140
+ (e) => typeof e === "object" && e !== null && !Array.isArray(e)
1141
+ );
1142
+ }
1143
+ var FORGE_BRAND_THEME_ID = "__forge_brand__";
1144
+ var BRAND_CONTENT_SLOT_KEYS = {
1145
+ primary: "content-primary",
1146
+ accent: "content-accent",
1147
+ bg: "content-bg",
1148
+ card: "content-card",
1149
+ text: "content-text"
1150
+ };
1151
+ function parseBrandThemeContent(content) {
1152
+ let value = content;
1153
+ if (typeof value === "string") {
1154
+ try {
1155
+ value = JSON.parse(value);
1156
+ } catch {
1157
+ return [];
1158
+ }
1159
+ }
1160
+ if (!Array.isArray(value)) return [];
1161
+ return value.filter(
1162
+ (e) => typeof e === "object" && e !== null && !Array.isArray(e)
1163
+ );
1164
+ }
1165
+ function brandThemeConfigToEntry(config) {
1166
+ if (!config || typeof config !== "object") return null;
1167
+ const values = config.values;
1168
+ if (!values || typeof values !== "object") return null;
1169
+ const entry = {
1170
+ id: FORGE_BRAND_THEME_ID,
1171
+ label: config.name ?? "Brand",
1172
+ primary: values[BRAND_CONTENT_SLOT_KEYS.primary],
1173
+ accent: values[BRAND_CONTENT_SLOT_KEYS.accent],
1174
+ bg: values[BRAND_CONTENT_SLOT_KEYS.bg],
1175
+ card: values[BRAND_CONTENT_SLOT_KEYS.card],
1176
+ text: values[BRAND_CONTENT_SLOT_KEYS.text]
1177
+ };
1178
+ return themeEntryToPalette(entry) ? entry : null;
1179
+ }
1180
+ function resolveBrandPalette(input) {
1181
+ if (isNonEmptyString(input.selectedThemeId)) {
1182
+ const picked = input.themes.find((t) => t.id === input.selectedThemeId);
1183
+ const palette = themeEntryToPalette(picked);
1184
+ if (palette) return palette;
1185
+ }
1186
+ return input.scope === "app" ? input.appDefault : input.frameworkDefault;
1187
+ }
1188
+
1189
+ // src/forge/schemaToForm.ts
1190
+ var MULTILINE_KEYWORDS = /body|brief|description|content|text|copy|hashtags|caption|outline|notes|story|paragraph/i;
1191
+ var MULTILINE_MAX_LENGTH = 200;
1192
+ function isPlainObject5(v) {
1193
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1194
+ }
1195
+ function humanise(name) {
1196
+ return name.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim().replace(/\b\w/g, (c) => c.toUpperCase());
1197
+ }
1198
+ function isMultilineString(name, def) {
1199
+ const max = typeof def.maxLength === "number" ? def.maxLength : void 0;
1200
+ if (max !== void 0) return max > MULTILINE_MAX_LENGTH;
1201
+ return MULTILINE_KEYWORDS.test(name);
1202
+ }
1203
+ function resolveProperty(name, def, required) {
1204
+ const description = typeof def.description === "string" ? def.description : void 0;
1205
+ const label = humanise(name);
1206
+ const t = def.type;
1207
+ if (t === "string") {
1208
+ const enumVals = Array.isArray(def.enum) ? def.enum.filter((x) => typeof x === "string") : void 0;
1209
+ return {
1210
+ kind: "string",
1211
+ name,
1212
+ label,
1213
+ required,
1214
+ description,
1215
+ multiline: enumVals ? false : isMultilineString(name, def),
1216
+ ...enumVals ? { enum: enumVals } : {},
1217
+ ...typeof def.maxLength === "number" ? { maxLength: def.maxLength } : {},
1218
+ ...typeof def.minLength === "number" ? { minLength: def.minLength } : {}
1219
+ };
1220
+ }
1221
+ if (t === "number" || t === "integer") {
1222
+ return {
1223
+ kind: "number",
1224
+ name,
1225
+ label,
1226
+ required,
1227
+ description,
1228
+ integer: t === "integer",
1229
+ ...typeof def.minimum === "number" ? { minimum: def.minimum } : {},
1230
+ ...typeof def.maximum === "number" ? { maximum: def.maximum } : {}
1231
+ };
1232
+ }
1233
+ if (t === "boolean") {
1234
+ return { kind: "boolean", name, label, required, description };
1235
+ }
1236
+ if (t === "array") {
1237
+ const items = isPlainObject5(def.items) ? def.items : null;
1238
+ const itemField = items ? resolveProperty("item", items, true) : null;
1239
+ return {
1240
+ kind: "array",
1241
+ name,
1242
+ label,
1243
+ required,
1244
+ description,
1245
+ itemField,
1246
+ ...typeof def.minItems === "number" ? { minItems: def.minItems } : {},
1247
+ ...typeof def.maxItems === "number" ? { maxItems: def.maxItems } : {}
1248
+ };
1249
+ }
1250
+ if (t === "object" && isPlainObject5(def.properties)) {
1251
+ const reqList = Array.isArray(def.required) ? def.required.filter((x) => typeof x === "string") : [];
1252
+ const fields = [];
1253
+ for (const [k, d] of Object.entries(def.properties)) {
1254
+ if (!isPlainObject5(d)) return null;
1255
+ const f = resolveProperty(k, d, reqList.includes(k));
1256
+ if (!f) return null;
1257
+ fields.push(f);
1258
+ }
1259
+ return { kind: "object", name, label, required, description, fields };
1260
+ }
1261
+ return null;
1262
+ }
1263
+ function schemaToForm(schema) {
1264
+ if (!isPlainObject5(schema)) {
1265
+ return { kind: "raw", reason: "schema_json is not an object" };
1266
+ }
1267
+ if (schema.type === "object" && isPlainObject5(schema.properties)) {
1268
+ const reqList = Array.isArray(schema.required) ? schema.required.filter((x) => typeof x === "string") : [];
1269
+ const fields = [];
1270
+ for (const [name, def] of Object.entries(schema.properties)) {
1271
+ if (!isPlainObject5(def)) {
1272
+ return {
1273
+ kind: "raw",
1274
+ reason: `property '${name}' is not an object \u2014 cannot render`
1275
+ };
1276
+ }
1277
+ const f = resolveProperty(name, def, reqList.includes(name));
1278
+ if (!f) {
1279
+ return {
1280
+ kind: "raw",
1281
+ reason: `property '${name}' uses an unsupported type \u2014 cannot render`
1282
+ };
1283
+ }
1284
+ fields.push(f);
1285
+ }
1286
+ if (fields.length === 0) {
1287
+ return { kind: "raw", reason: "schema has no properties to render" };
1288
+ }
1289
+ return { kind: "form", fields };
1290
+ }
1291
+ if (Array.isArray(schema.sections)) {
1292
+ const sections = schema.sections.filter(
1293
+ (x) => typeof x === "string"
1294
+ );
1295
+ if (sections.length === 0) {
1296
+ return { kind: "raw", reason: "sections array is empty" };
1297
+ }
1298
+ const fields = sections.map((name) => ({
1299
+ kind: "string",
1300
+ name,
1301
+ label: humanise(name),
1302
+ required: true,
1303
+ multiline: MULTILINE_KEYWORDS.test(name)
1304
+ }));
1305
+ return { kind: "form", fields };
1306
+ }
1307
+ return { kind: "raw", reason: "schema shape not form-compatible" };
1308
+ }
1309
+ function defaultValueForField(field) {
1310
+ switch (field.kind) {
1311
+ case "string":
1312
+ return "";
1313
+ case "number":
1314
+ return "";
1315
+ case "boolean":
1316
+ return false;
1317
+ case "array":
1318
+ return [];
1319
+ case "object":
1320
+ return initialFormValues(field.fields, {});
1321
+ }
1322
+ }
1323
+ function pickInitialValue(field, current) {
1324
+ switch (field.kind) {
1325
+ case "string":
1326
+ return typeof current === "string" ? current : "";
1327
+ case "number":
1328
+ return typeof current === "number" ? current : "";
1329
+ case "boolean":
1330
+ return typeof current === "boolean" ? current : false;
1331
+ case "array":
1332
+ return Array.isArray(current) ? current.slice() : [];
1333
+ case "object": {
1334
+ const c = isPlainObject5(current) ? current : {};
1335
+ return initialFormValues(field.fields, c);
1336
+ }
1337
+ }
1338
+ }
1339
+ function initialFormValues(fields, source) {
1340
+ const out = {};
1341
+ for (const f of fields) {
1342
+ out[f.name] = source[f.name] !== void 0 ? pickInitialValue(f, source[f.name]) : defaultValueForField(f);
1343
+ }
1344
+ return out;
1345
+ }
1346
+ function isEmptyString(v) {
1347
+ return typeof v === "string" && v.trim().length === 0;
1348
+ }
1349
+ function validateField(field, value, path) {
1350
+ const errors = {};
1351
+ switch (field.kind) {
1352
+ case "string": {
1353
+ if (field.required && (value === void 0 || value === null || isEmptyString(value))) {
1354
+ errors[path] = `${field.label} ist erforderlich`;
1355
+ break;
1356
+ }
1357
+ if (typeof value === "string") {
1358
+ if (field.minLength !== void 0 && value.length < field.minLength) {
1359
+ errors[path] = `${field.label} mindestens ${field.minLength} Zeichen`;
1360
+ } else if (field.maxLength !== void 0 && value.length > field.maxLength) {
1361
+ errors[path] = `${field.label} h\xF6chstens ${field.maxLength} Zeichen`;
1362
+ } else if (field.enum && value.length > 0 && !field.enum.includes(value)) {
1363
+ errors[path] = `${field.label}: Wert nicht in der Auswahl`;
1364
+ }
1365
+ }
1366
+ break;
1367
+ }
1368
+ case "number": {
1369
+ if (value === "" || value === void 0 || value === null) {
1370
+ if (field.required) errors[path] = `${field.label} ist erforderlich`;
1371
+ break;
1372
+ }
1373
+ const n = typeof value === "number" ? value : Number(value);
1374
+ if (!Number.isFinite(n)) {
1375
+ errors[path] = `${field.label} muss eine Zahl sein`;
1376
+ } else if (field.integer && !Number.isInteger(n)) {
1377
+ errors[path] = `${field.label} muss eine ganze Zahl sein`;
1378
+ } else if (field.minimum !== void 0 && n < field.minimum) {
1379
+ errors[path] = `${field.label} >= ${field.minimum}`;
1380
+ } else if (field.maximum !== void 0 && n > field.maximum) {
1381
+ errors[path] = `${field.label} <= ${field.maximum}`;
1382
+ }
1383
+ break;
1384
+ }
1385
+ case "boolean":
1386
+ if (field.required && value !== true) {
1387
+ errors[path] = `${field.label} muss aktiviert sein`;
1388
+ }
1389
+ break;
1390
+ case "array": {
1391
+ const arr = Array.isArray(value) ? value : [];
1392
+ if (field.required && arr.length === 0) {
1393
+ errors[path] = `${field.label}: mindestens ein Eintrag erforderlich`;
1394
+ }
1395
+ if (field.minItems !== void 0 && arr.length < field.minItems) {
1396
+ errors[path] = `${field.label}: mindestens ${field.minItems} Eintr\xE4ge`;
1397
+ } else if (field.maxItems !== void 0 && arr.length > field.maxItems) {
1398
+ errors[path] = `${field.label}: h\xF6chstens ${field.maxItems} Eintr\xE4ge`;
1399
+ }
1400
+ if (field.itemField) {
1401
+ arr.forEach((item, i) => {
1402
+ const sub = validateField(
1403
+ field.itemField,
1404
+ item,
1405
+ `${path}[${i}]`
1406
+ );
1407
+ Object.assign(errors, sub);
1408
+ });
1409
+ }
1410
+ break;
1411
+ }
1412
+ case "object": {
1413
+ const obj = isPlainObject5(value) ? value : {};
1414
+ for (const sub of field.fields) {
1415
+ const subErrors = validateField(sub, obj[sub.name], `${path}.${sub.name}`);
1416
+ Object.assign(errors, subErrors);
1417
+ }
1418
+ break;
1419
+ }
1420
+ }
1421
+ return errors;
1422
+ }
1423
+ function validateFormValues(fields, values) {
1424
+ const errors = {};
1425
+ for (const f of fields) {
1426
+ Object.assign(errors, validateField(f, values[f.name], f.name));
1427
+ }
1428
+ return errors;
1429
+ }
1430
+ var TEMPLATE_SCHEMA_EXAMPLES = {
1431
+ long_article: {
1432
+ type: "object",
1433
+ required: ["headline", "lede", "sections"],
1434
+ properties: {
1435
+ headline: { type: "string" },
1436
+ lede: { type: "string" },
1437
+ sections: {
1438
+ type: "array",
1439
+ items: {
1440
+ type: "object",
1441
+ properties: { h2: { type: "string" }, body: { type: "string" } },
1442
+ required: ["h2", "body"]
1443
+ }
1444
+ }
1445
+ }
1446
+ },
1447
+ short_post: {
1448
+ type: "object",
1449
+ required: ["hook", "body"],
1450
+ properties: {
1451
+ hook: { type: "string", maxLength: 200 },
1452
+ body: { type: "string", maxLength: 1200 },
1453
+ cta: { type: "string" }
1454
+ }
1455
+ },
1456
+ carousel_sequence: {
1457
+ type: "object",
1458
+ required: ["slides"],
1459
+ properties: {
1460
+ slides: {
1461
+ type: "array",
1462
+ items: {
1463
+ type: "object",
1464
+ properties: {
1465
+ headline: { type: "string" },
1466
+ body: { type: "string" },
1467
+ visual_brief: { type: "string" }
1468
+ },
1469
+ required: ["headline", "body"]
1470
+ }
1471
+ }
1472
+ }
1473
+ }
1474
+ };
1475
+ var TEMPLATE_SCHEMA_GENERIC = {
1476
+ type: "object",
1477
+ required: ["body"],
1478
+ properties: { body: { type: "string" } }
1479
+ };
1480
+ function schemaExampleFor(format) {
1481
+ return TEMPLATE_SCHEMA_EXAMPLES[format] ?? TEMPLATE_SCHEMA_GENERIC;
1482
+ }
1483
+
1484
+ // src/forge/engine.ts
1485
+ async function runForgeGeneration(input) {
1486
+ const { storage, provider } = input;
1487
+ const [signal, template] = await Promise.all([
1488
+ storage.getSignal(input.signalId),
1489
+ storage.getTemplate(input.templateId)
1490
+ ]);
1491
+ if (!signal) {
1492
+ return { kind: "error", status: 404, error: "signal not found", field: "signalId" };
1493
+ }
1494
+ if (!template) {
1495
+ return { kind: "error", status: 404, error: "template not found", field: "templateId" };
1496
+ }
1497
+ const resolved = {
1498
+ channel: template.channel,
1499
+ audience_id: input.audienceOverride ?? String(template.audience_id),
1500
+ intent: input.intentOverride ?? template.intent,
1501
+ format: input.formatOverride ?? template.format,
1502
+ modality: input.modalityOverride ?? template.modality
1503
+ };
1504
+ const audience = await storage.getAudience(resolved.audience_id);
1505
+ if (!audience) {
1506
+ return {
1507
+ kind: "error",
1508
+ status: 404,
1509
+ error: "audience not found",
1510
+ field: "audienceOverride"
1511
+ };
1512
+ }
1513
+ const productTruth = distill({
1514
+ signal,
1515
+ audience
1516
+ });
1517
+ const prompt = assembleForgePrompt({
1518
+ template,
1519
+ audience,
1520
+ productTruth,
1521
+ resolved
1522
+ });
1523
+ if (input.dryRun) {
1524
+ return {
1525
+ kind: "dry_run",
1526
+ payload: {
1527
+ dryRun: true,
1528
+ resolved,
1529
+ product_truth: productTruth,
1530
+ prompt: {
1531
+ system: prompt.system,
1532
+ user: prompt.user,
1533
+ schema_name: prompt.schema_name
1534
+ }
1535
+ }
1536
+ };
1537
+ }
1538
+ let rawResponse = "";
1539
+ let parseError = null;
1540
+ let parsed = null;
1541
+ let validation = null;
1542
+ try {
1543
+ const result = await provider.complete({
1544
+ systemPrompt: prompt.system,
1545
+ messages: [{ role: "user", content: prompt.user }],
1546
+ maxTokens: input.maxTokens ?? 4096
1547
+ });
1548
+ rawResponse = result.content;
1549
+ } catch (err) {
1550
+ parseError = err instanceof Error ? err.message : String(err);
1551
+ }
1552
+ if (!parseError) {
1553
+ try {
1554
+ parsed = tryParseJsonObject(rawResponse);
1555
+ validation = validateAgainstTemplateSchema(
1556
+ template.schema_json,
1557
+ parsed
1558
+ );
1559
+ } catch (e) {
1560
+ parseError = e instanceof Error ? e.message : String(e);
1561
+ }
1562
+ }
1563
+ const status = parseError === null && validation && validation.ok ? "ready" : "failed";
1564
+ const result_json = status === "ready" ? { content: parsed, product_truth: productTruth } : {
1565
+ failure_reason: parseError ?? (validation && !validation.ok ? validation.errors.join("; ") : "unknown"),
1566
+ raw: rawResponse.slice(0, 4e3)
1567
+ };
1568
+ const output = await storage.saveOutput({
1569
+ signal_id: signal.id,
1570
+ template_id: template.id,
1571
+ status,
1572
+ scope_kind: signal.scope_kind,
1573
+ app_id: signal.scope_kind === "app" ? signal.app_id ?? null : null,
1574
+ env: signal.scope_kind === "app" ? signal.env ?? null : null,
1575
+ channel: resolved.channel,
1576
+ audience_id: resolved.audience_id,
1577
+ intent: resolved.intent,
1578
+ format: resolved.format,
1579
+ modality: resolved.modality,
1580
+ result_json
1581
+ });
1582
+ return {
1583
+ kind: "generated",
1584
+ status: status === "ready" ? 201 : 200,
1585
+ payload: {
1586
+ output,
1587
+ validation: validation ?? null,
1588
+ failure_reason: status === "failed" ? result_json.failure_reason ?? null : null
1589
+ }
1590
+ };
1591
+ }
1592
+
1593
+ // src/forge/widget.ts
1594
+ function getSlotValue(slots, template, slotId) {
1595
+ const val = slots[slotId];
1596
+ if (val !== void 0 && val !== null && val !== "") return String(val);
1597
+ const def = template.slots.find((s) => s.id === slotId);
1598
+ return def?.default !== void 0 ? String(def.default) : "";
1599
+ }
1600
+
1601
+ // src/forge/widgetStyle.ts
1602
+ var BASE_SPACING = {
1603
+ xs: "4px",
1604
+ sm: "8px",
1605
+ md: "16px",
1606
+ lg: "24px",
1607
+ xl: "40px"
1608
+ };
1609
+ var STYLE_PRESET_DEFAULT = {
1610
+ id: "default",
1611
+ name: "Default",
1612
+ preset: true,
1613
+ tokens: {
1614
+ radius: { sm: "6px", md: "12px", lg: "20px", full: "9999px" },
1615
+ shadow: {
1616
+ sm: "0 1px 3px rgba(0,0,0,.12)",
1617
+ md: "0 4px 16px rgba(0,0,0,.15)",
1618
+ lg: "0 8px 32px rgba(0,0,0,.20)"
1619
+ },
1620
+ spacing: BASE_SPACING,
1621
+ borderWidth: "1px",
1622
+ borderStyle: "solid",
1623
+ animation: { entry: "fade", durationMs: 300 }
1624
+ },
1625
+ version: 1,
1626
+ versions: [],
1627
+ created_at: "",
1628
+ updated_at: ""
1629
+ };
1630
+ var STYLE_PRESET_MINIMAL = {
1631
+ id: "minimal",
1632
+ name: "Minimal",
1633
+ preset: true,
1634
+ tokens: {
1635
+ radius: { sm: "0px", md: "0px", lg: "0px", full: "0px" },
1636
+ shadow: { sm: "none", md: "none", lg: "none" },
1637
+ spacing: BASE_SPACING,
1638
+ borderWidth: "1px",
1639
+ borderStyle: "solid",
1640
+ animation: { entry: "none", durationMs: 0 }
1641
+ },
1642
+ version: 1,
1643
+ versions: [],
1644
+ created_at: "",
1645
+ updated_at: ""
1646
+ };
1647
+ var STYLE_PRESET_GLASSY = {
1648
+ id: "glassy",
1649
+ name: "Glassy",
1650
+ preset: true,
1651
+ tokens: {
1652
+ radius: { sm: "12px", md: "20px", lg: "28px", full: "9999px" },
1653
+ shadow: {
1654
+ sm: "0 2px 8px rgba(0,0,0,.08), inset 0 1px 0 rgba(255,255,255,.06)",
1655
+ md: "0 8px 24px rgba(0,0,0,.12), inset 0 1px 0 rgba(255,255,255,.08)",
1656
+ lg: "0 16px 48px rgba(0,0,0,.16), inset 0 1px 0 rgba(255,255,255,.10)"
1657
+ },
1658
+ spacing: BASE_SPACING,
1659
+ borderWidth: "1px",
1660
+ borderStyle: "solid",
1661
+ animation: { entry: "scale", durationMs: 250 }
1662
+ },
1663
+ customCss: ".pw-widget{backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);}",
1664
+ version: 1,
1665
+ versions: [],
1666
+ created_at: "",
1667
+ updated_at: ""
1668
+ };
1669
+ var STYLE_PRESET_BRUTALIST = {
1670
+ id: "brutalist",
1671
+ name: "Brutalist",
1672
+ preset: true,
1673
+ tokens: {
1674
+ radius: { sm: "0px", md: "0px", lg: "0px", full: "0px" },
1675
+ shadow: {
1676
+ sm: "2px 2px 0 currentColor",
1677
+ md: "4px 4px 0 currentColor",
1678
+ lg: "6px 6px 0 currentColor"
1679
+ },
1680
+ spacing: BASE_SPACING,
1681
+ borderWidth: "2px",
1682
+ borderStyle: "solid",
1683
+ animation: { entry: "slide", durationMs: 200 }
1684
+ },
1685
+ version: 1,
1686
+ versions: [],
1687
+ created_at: "",
1688
+ updated_at: ""
1689
+ };
1690
+ var BUNDLED_STYLE_PRESETS = [
1691
+ STYLE_PRESET_DEFAULT,
1692
+ STYLE_PRESET_MINIMAL,
1693
+ STYLE_PRESET_GLASSY,
1694
+ STYLE_PRESET_BRUTALIST
1695
+ ];
1696
+ function getStyleById(id) {
1697
+ return BUNDLED_STYLE_PRESETS.find((s) => s.id === id) ?? STYLE_PRESET_DEFAULT;
1698
+ }
1699
+ function safeStr2(v, fallback) {
1700
+ return typeof v === "string" && v.trim() ? v.trim() : fallback;
1701
+ }
1702
+ function safeNum2(v, fallback) {
1703
+ const n = Number(v);
1704
+ return Number.isFinite(n) ? n : fallback;
1705
+ }
1706
+ function parseStyleFromTokensJson(json) {
1707
+ if (!json || typeof json !== "object") return {};
1708
+ const obj = json;
1709
+ const get = (keys) => {
1710
+ let cur = obj;
1711
+ for (const k of keys) {
1712
+ if (!cur || typeof cur !== "object") return void 0;
1713
+ cur = cur[k];
1714
+ }
1715
+ return cur?.value ?? cur;
1716
+ };
1717
+ const radius = {};
1718
+ const shadow = {};
1719
+ const spacing = {};
1720
+ const radiusSm = get(["borderRadius", "sm"]) ?? get(["radius", "sm"]) ?? get(["border-radius", "sm"]);
1721
+ const radiusMd = get(["borderRadius", "md"]) ?? get(["radius", "md"]) ?? get(["border-radius", "md"]);
1722
+ const radiusLg = get(["borderRadius", "lg"]) ?? get(["radius", "lg"]) ?? get(["border-radius", "lg"]);
1723
+ if (radiusSm) radius.sm = safeStr2(radiusSm, "6px");
1724
+ if (radiusMd) radius.md = safeStr2(radiusMd, "12px");
1725
+ if (radiusLg) radius.lg = safeStr2(radiusLg, "20px");
1726
+ const shadowSm = get(["boxShadow", "sm"]) ?? get(["shadow", "sm"]);
1727
+ const shadowMd = get(["boxShadow", "md"]) ?? get(["shadow", "md"]) ?? get(["boxShadow", "DEFAULT"]);
1728
+ const shadowLg = get(["boxShadow", "lg"]) ?? get(["shadow", "lg"]);
1729
+ if (shadowSm) shadow.sm = safeStr2(shadowSm, STYLE_PRESET_DEFAULT.tokens.shadow.sm);
1730
+ if (shadowMd) shadow.md = safeStr2(shadowMd, STYLE_PRESET_DEFAULT.tokens.shadow.md);
1731
+ if (shadowLg) shadow.lg = safeStr2(shadowLg, STYLE_PRESET_DEFAULT.tokens.shadow.lg);
1732
+ const spacingSm = get(["spacing", "sm"]) ?? get(["space", "sm"]);
1733
+ const spacingMd = get(["spacing", "md"]) ?? get(["space", "md"]);
1734
+ const spacingLg = get(["spacing", "lg"]) ?? get(["space", "lg"]);
1735
+ if (spacingSm) spacing.sm = safeStr2(spacingSm, "8px");
1736
+ if (spacingMd) spacing.md = safeStr2(spacingMd, "16px");
1737
+ if (spacingLg) spacing.lg = safeStr2(spacingLg, "24px");
1738
+ const animDuration = get(["animation", "duration"]) ?? get(["motion", "duration"]);
1739
+ return {
1740
+ ...Object.keys(radius).length ? { radius: { ...STYLE_PRESET_DEFAULT.tokens.radius, ...radius } } : {},
1741
+ ...Object.keys(shadow).length ? { shadow: { ...STYLE_PRESET_DEFAULT.tokens.shadow, ...shadow } } : {},
1742
+ ...Object.keys(spacing).length ? { spacing: { ...STYLE_PRESET_DEFAULT.tokens.spacing, ...spacing } } : {},
1743
+ ...animDuration ? { animation: { ...STYLE_PRESET_DEFAULT.tokens.animation, durationMs: safeNum2(animDuration, 300) } } : {}
1744
+ };
1745
+ }
1746
+ function parseStyleFromTailwindConfig(jsSource) {
1747
+ const result = {};
1748
+ const radiusMatch = {};
1749
+ const radiusRx = /['"]?(?:sm|md|lg|DEFAULT)['"]?\s*:\s*['"]([^'"]+)['"]/g;
1750
+ const radiusSection = /borderRadius\s*:\s*\{([^}]+)\}/.exec(jsSource)?.[1] ?? "";
1751
+ let m;
1752
+ while ((m = radiusRx.exec(radiusSection)) !== null) {
1753
+ radiusMatch[m[0].split(":")[0].replace(/['"\s]/g, "")] = m[1];
1754
+ }
1755
+ if (radiusMatch.sm || radiusMatch.md || radiusMatch.lg) {
1756
+ result.radius = {
1757
+ sm: radiusMatch.sm ?? STYLE_PRESET_DEFAULT.tokens.radius.sm,
1758
+ md: radiusMatch.md ?? radiusMatch.DEFAULT ?? STYLE_PRESET_DEFAULT.tokens.radius.md,
1759
+ lg: radiusMatch.lg ?? STYLE_PRESET_DEFAULT.tokens.radius.lg,
1760
+ full: STYLE_PRESET_DEFAULT.tokens.radius.full
1761
+ };
1762
+ }
1763
+ const spacingSection = /spacing\s*:\s*\{([^}]+)\}/.exec(jsSource)?.[1] ?? "";
1764
+ const spMap = {};
1765
+ const spRx = /['"]?(\d+|sm|md|lg|xs|xl)['"]?\s*:\s*['"]([^'"]+)['"]/g;
1766
+ while ((m = spRx.exec(spacingSection)) !== null) {
1767
+ spMap[m[1]] = m[2];
1768
+ }
1769
+ if (Object.keys(spMap).length) {
1770
+ result.spacing = {
1771
+ xs: spMap.xs ?? spMap["1"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.xs,
1772
+ sm: spMap.sm ?? spMap["2"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.sm,
1773
+ md: spMap.md ?? spMap["4"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.md,
1774
+ lg: spMap.lg ?? spMap["6"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.lg,
1775
+ xl: spMap.xl ?? spMap["10"] ?? STYLE_PRESET_DEFAULT.tokens.spacing.xl
1776
+ };
1777
+ }
1778
+ return result;
1779
+ }
1780
+ function parseStyleFromCss(css) {
1781
+ const tokens = {};
1782
+ const radiusSm = /--radius-sm\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
1783
+ const radiusMd = /--radius(?:-md)?\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
1784
+ const radiusLg = /--radius-lg\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
1785
+ if (radiusSm || radiusMd || radiusLg) {
1786
+ tokens.radius = {
1787
+ sm: radiusSm ?? STYLE_PRESET_DEFAULT.tokens.radius.sm,
1788
+ md: radiusMd ?? STYLE_PRESET_DEFAULT.tokens.radius.md,
1789
+ lg: radiusLg ?? STYLE_PRESET_DEFAULT.tokens.radius.lg,
1790
+ full: STYLE_PRESET_DEFAULT.tokens.radius.full
1791
+ };
1792
+ }
1793
+ const shadowMd = /--shadow(?:-md)?\s*:\s*([^;]+)/.exec(css)?.[1]?.trim();
1794
+ if (shadowMd) {
1795
+ tokens.shadow = { ...STYLE_PRESET_DEFAULT.tokens.shadow, md: shadowMd };
1796
+ }
1797
+ return { tokens, customCss: css };
1798
+ }
1799
+
1800
+ // src/forge/widgetTemplates.ts
1801
+ var WIDGET_TEMPLATE_STAT_CARD = {
1802
+ id: "stat-card",
1803
+ name: "Stat Card",
1804
+ description: "Big number with label and optional delta arrow.",
1805
+ free_tier: true,
1806
+ exportFormats: ["html", "markdown", "react"],
1807
+ slots: [
1808
+ { id: "value", label: "Value", type: "text", required: true, placeholder: "138", default: "138" },
1809
+ { id: "label", label: "Label", type: "text", required: true, placeholder: "Total Files", default: "Total Files" },
1810
+ { id: "delta", label: "Delta", type: "text", placeholder: "+12%", default: "" },
1811
+ { id: "delta_direction", label: "Delta direction", type: "select", options: ["up", "down", "neutral"], default: "up" },
1812
+ { id: "unit", label: "Unit (optional)", type: "text", placeholder: "files", default: "" }
1813
+ ]
1814
+ };
1815
+ var WIDGET_TEMPLATE_FEATURE_GRID = {
1816
+ id: "feature-grid",
1817
+ name: "Feature Grid",
1818
+ description: "Grid of features with icons or checkmarks.",
1819
+ free_tier: false,
1820
+ exportFormats: ["html", "markdown", "react"],
1821
+ slots: [
1822
+ { id: "title", label: "Title", type: "text", placeholder: "What you get", default: "What you get" },
1823
+ { 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" },
1824
+ { id: "columns", label: "Columns", type: "select", options: ["2", "3"], default: "3" },
1825
+ { id: "icon", label: "Icon prefix", type: "text", placeholder: "\u2713", default: "\u2713" }
1826
+ ]
1827
+ };
1828
+ var WIDGET_TEMPLATE_TESTIMONIAL = {
1829
+ id: "testimonial",
1830
+ name: "Testimonial",
1831
+ description: "Pull-quote with author, role, and optional photo.",
1832
+ free_tier: false,
1833
+ exportFormats: ["html", "markdown", "react"],
1834
+ slots: [
1835
+ { id: "quote", label: "Quote", type: "multiline", required: true, placeholder: "This tool changed how we ship.", default: "This tool changed how we ship." },
1836
+ { id: "author", label: "Author name", type: "text", required: true, placeholder: "Jane Smith", default: "Jane Smith" },
1837
+ { id: "role", label: "Role", type: "text", placeholder: "CTO", default: "CTO" },
1838
+ { id: "company", label: "Company", type: "text", placeholder: "Acme Corp", default: "Acme Corp" },
1839
+ { id: "image_url", label: "Author photo URL", type: "image-url", placeholder: "https://\u2026", default: "" }
1840
+ ]
1841
+ };
1842
+ var WIDGET_TEMPLATE_CTA_BANNER = {
1843
+ id: "cta-banner",
1844
+ name: "CTA Banner",
1845
+ description: "Full-width call-to-action with heading, subtitle, and button.",
1846
+ free_tier: true,
1847
+ exportFormats: ["html", "markdown", "react"],
1848
+ slots: [
1849
+ { id: "heading", label: "Heading", type: "text", required: true, placeholder: "Ship faster.", default: "Ship faster." },
1850
+ { id: "subtitle", label: "Subtitle", type: "text", placeholder: "From blueprint to production in minutes.", default: "From blueprint to production in minutes." },
1851
+ { id: "button_text", label: "Button text", type: "text", required: true, placeholder: "Get started", default: "Get started" },
1852
+ { id: "button_url", label: "Button URL", type: "url", placeholder: "https://\u2026", default: "#" },
1853
+ { id: "align", label: "Alignment", type: "select", options: ["left", "center"], default: "center" }
1854
+ ]
1855
+ };
1856
+ var WIDGET_TEMPLATE_METRIC_BADGE = {
1857
+ id: "metric-badge",
1858
+ name: "Metric Badge",
1859
+ description: "Small inline metric chip \u2014 uptime, users, coverage.",
1860
+ free_tier: true,
1861
+ exportFormats: ["html", "markdown", "react"],
1862
+ slots: [
1863
+ { id: "metric_name", label: "Metric name", type: "text", required: true, placeholder: "Uptime", default: "Uptime" },
1864
+ { id: "value", label: "Value", type: "text", required: true, placeholder: "99.9", default: "99.9" },
1865
+ { id: "unit", label: "Unit", type: "text", placeholder: "%", default: "%" },
1866
+ { id: "color", label: "Color override", type: "color", default: "" }
1867
+ ]
1868
+ };
1869
+ var WIDGET_TEMPLATE_PRICING_TIER = {
1870
+ id: "pricing-tier",
1871
+ name: "Pricing Tier",
1872
+ description: "Plan name, price, feature bullets, and CTA button.",
1873
+ free_tier: false,
1874
+ exportFormats: ["html", "markdown", "react"],
1875
+ slots: [
1876
+ { id: "plan_name", label: "Plan name", type: "text", required: true, placeholder: "Pro", default: "Pro" },
1877
+ { id: "price", label: "Price", type: "text", required: true, placeholder: "\u20AC49", default: "\u20AC49" },
1878
+ { id: "period", label: "Period", type: "text", placeholder: "/month", default: "/month" },
1879
+ { 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" },
1880
+ { id: "cta_text", label: "CTA text", type: "text", placeholder: "Get Pro", default: "Get Pro" },
1881
+ { id: "cta_url", label: "CTA URL", type: "url", placeholder: "https://\u2026", default: "#" },
1882
+ { id: "highlighted", label: "Highlighted tier", type: "select", options: ["yes", "no"], default: "no" }
1883
+ ]
1884
+ };
1885
+ var WIDGET_TEMPLATE_CHANGELOG_ROW = {
1886
+ id: "changelog-row",
1887
+ name: "Changelog Row",
1888
+ description: "Version, date, and bullet list of changes.",
1889
+ free_tier: false,
1890
+ exportFormats: ["html", "markdown", "react"],
1891
+ slots: [
1892
+ { id: "version", label: "Version", type: "text", required: true, placeholder: "v1.2.0", default: "v1.2.0" },
1893
+ { id: "date", label: "Date", type: "text", required: true, placeholder: "2024-01-15", default: "2024-01-15" },
1894
+ { id: "label", label: "Release label", type: "select", options: ["feat", "fix", "docs", "chore", ""], default: "feat" },
1895
+ { 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" }
1896
+ ]
1897
+ };
1898
+ var WIDGET_TEMPLATE_SOCIAL_PROOF = {
1899
+ id: "social-proof",
1900
+ name: "Social Proof",
1901
+ description: "Logo row with optional caption.",
1902
+ free_tier: false,
1903
+ exportFormats: ["html", "markdown", "react"],
1904
+ slots: [
1905
+ { id: "caption", label: "Caption", type: "text", placeholder: "Trusted by engineering teams at", default: "Trusted by engineering teams at" },
1906
+ { id: "logos", label: "Brand names (one per line)", type: "multiline", required: true, placeholder: "Vercel\nStripe\nLinear\nResend", default: "Acme Corp\nBeta Inc\nGamma Ltd" },
1907
+ { id: "show_divider", label: "Show divider", type: "select", options: ["yes", "no"], default: "no" }
1908
+ ]
1909
+ };
1910
+ var BUNDLED_WIDGET_TEMPLATES = [
1911
+ WIDGET_TEMPLATE_STAT_CARD,
1912
+ WIDGET_TEMPLATE_FEATURE_GRID,
1913
+ WIDGET_TEMPLATE_TESTIMONIAL,
1914
+ WIDGET_TEMPLATE_CTA_BANNER,
1915
+ WIDGET_TEMPLATE_METRIC_BADGE,
1916
+ WIDGET_TEMPLATE_PRICING_TIER,
1917
+ WIDGET_TEMPLATE_CHANGELOG_ROW,
1918
+ WIDGET_TEMPLATE_SOCIAL_PROOF
1919
+ ];
1920
+ var FREE_TIER_WIDGET_IDS = BUNDLED_WIDGET_TEMPLATES.filter((t) => t.free_tier).map((t) => t.id);
1921
+ function getWidgetTemplate(id) {
1922
+ return BUNDLED_WIDGET_TEMPLATES.find((t) => t.id === id);
1923
+ }
1924
+
1925
+ // src/forge/widgetRenderer.ts
1926
+ function brandVars(brand) {
1927
+ const p = brand?.palette;
1928
+ const f = brand?.fonts;
1929
+ return {
1930
+ "--pw-brand-primary": p?.primary ?? "#f97316",
1931
+ "--pw-brand-secondary": p?.secondary ?? "#1e293b",
1932
+ "--pw-brand-accent": p?.accent ?? "#fb923c",
1933
+ "--pw-brand-surface": p?.surface ?? "#0f172a",
1934
+ "--pw-brand-text": p?.text ?? "#f1f5f9",
1935
+ "--pw-brand-muted": p?.muted ?? "#64748b",
1936
+ "--pw-font-heading": f?.heading ? `'${f.heading}',sans-serif` : "sans-serif",
1937
+ "--pw-font-body": f?.body ? `'${f.body}',sans-serif` : "sans-serif",
1938
+ "--pw-font-mono": f?.mono ? `'${f.mono}',monospace` : "monospace"
1939
+ };
1940
+ }
1941
+ function styleVars(style) {
1942
+ const t = style.tokens;
1943
+ return {
1944
+ "--pw-radius-sm": t.radius.sm,
1945
+ "--pw-radius-md": t.radius.md,
1946
+ "--pw-radius-lg": t.radius.lg,
1947
+ "--pw-radius-full": t.radius.full,
1948
+ "--pw-shadow-sm": t.shadow.sm,
1949
+ "--pw-shadow-md": t.shadow.md,
1950
+ "--pw-shadow-lg": t.shadow.lg,
1951
+ "--pw-spacing-xs": t.spacing.xs,
1952
+ "--pw-spacing-sm": t.spacing.sm,
1953
+ "--pw-spacing-md": t.spacing.md,
1954
+ "--pw-spacing-lg": t.spacing.lg,
1955
+ "--pw-spacing-xl": t.spacing.xl,
1956
+ "--pw-border-width": t.borderWidth
1957
+ };
1958
+ }
1959
+ function buildCssVarString(vars) {
1960
+ return Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";");
1961
+ }
1962
+ function buildInlineStyle(vars) {
1963
+ return buildCssVarString(vars);
1964
+ }
1965
+ function escHtml(s) {
1966
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1967
+ }
1968
+ function baseWidgetStyle() {
1969
+ 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;}`;
1970
+ }
1971
+ function renderStatCardHtml(slots, t) {
1972
+ const value = getSlotValue(slots, t, "value");
1973
+ const label = getSlotValue(slots, t, "label");
1974
+ const delta = getSlotValue(slots, t, "delta");
1975
+ const dir = getSlotValue(slots, t, "delta_direction");
1976
+ const unit = getSlotValue(slots, t, "unit");
1977
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "\u2192";
1978
+ const deltaColor = dir === "up" ? "#22c55e" : dir === "down" ? "#ef4444" : "inherit";
1979
+ 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>
1980
+ <div class="pw-stat-label" style="font-size:.875rem;color:var(--pw-brand-muted);margin-top:var(--pw-spacing-xs)">${escHtml(label)}</div>
1981
+ ${delta ? `<div class="pw-stat-delta" style="font-size:.75rem;color:${deltaColor};margin-top:var(--pw-spacing-xs)">${arrow} ${escHtml(delta)}</div>` : ""}`;
1982
+ }
1983
+ function renderFeatureGridHtml(slots, t) {
1984
+ const title = getSlotValue(slots, t, "title");
1985
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
1986
+ const cols = getSlotValue(slots, t, "columns") || "3";
1987
+ const icon = getSlotValue(slots, t, "icon") || "\u2713";
1988
+ 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>` : ""}
1989
+ <div class="pw-grid" style="display:grid;grid-template-columns:repeat(${escHtml(cols)},1fr);gap:var(--pw-spacing-sm)">
1990
+ ${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")}
1991
+ </div>`;
1992
+ }
1993
+ function renderTestimonialHtml(slots, t) {
1994
+ const quote = getSlotValue(slots, t, "quote");
1995
+ const author = getSlotValue(slots, t, "author");
1996
+ const role = getSlotValue(slots, t, "role");
1997
+ const company = getSlotValue(slots, t, "company");
1998
+ const img = getSlotValue(slots, t, "image_url");
1999
+ return `<blockquote style="margin:0;font-size:1.125rem;line-height:1.6;font-style:italic;color:var(--pw-brand-text)">"${escHtml(quote)}"</blockquote>
2000
+ <div class="pw-attribution" style="display:flex;align-items:center;gap:var(--pw-spacing-sm);margin-top:var(--pw-spacing-md)">
2001
+ ${img ? ` <img src="${escHtml(img)}" alt="${escHtml(author)}" style="width:40px;height:40px;border-radius:var(--pw-radius-full);object-fit:cover" />` : ""}
2002
+ <div>
2003
+ <div style="font-weight:600;font-size:.875rem">${escHtml(author)}</div>
2004
+ ${role || company ? `<div style="font-size:.75rem;color:var(--pw-brand-muted)">${[role, company].filter(Boolean).map(escHtml).join(", ")}</div>` : ""}
2005
+ </div>
2006
+ </div>`;
2007
+ }
2008
+ function renderCtaBannerHtml(slots, t) {
2009
+ const heading = getSlotValue(slots, t, "heading");
2010
+ const subtitle = getSlotValue(slots, t, "subtitle");
2011
+ const btnText = getSlotValue(slots, t, "button_text");
2012
+ const btnUrl = getSlotValue(slots, t, "button_url") || "#";
2013
+ const align = getSlotValue(slots, t, "align") || "center";
2014
+ return `<div style="text-align:${escHtml(align)}">
2015
+ <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>
2016
+ ${subtitle ? `<p style="margin:0 0 var(--pw-spacing-md);font-size:1rem;color:var(--pw-brand-muted)">${escHtml(subtitle)}</p>` : ""}
2017
+ <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>
2018
+ </div>`;
2019
+ }
2020
+ function renderMetricBadgeHtml(slots, t) {
2021
+ const name = getSlotValue(slots, t, "metric_name");
2022
+ const value = getSlotValue(slots, t, "value");
2023
+ const unit = getSlotValue(slots, t, "unit");
2024
+ const color = getSlotValue(slots, t, "color") || "var(--pw-brand-accent)";
2025
+ 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">
2026
+ <span style="font-size:.75rem;color:var(--pw-brand-muted)">${escHtml(name)}</span>
2027
+ <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>
2028
+ </span>`;
2029
+ }
2030
+ function renderPricingTierHtml(slots, t) {
2031
+ const plan = getSlotValue(slots, t, "plan_name");
2032
+ const price = getSlotValue(slots, t, "price");
2033
+ const period = getSlotValue(slots, t, "period");
2034
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2035
+ const cta = getSlotValue(slots, t, "cta_text");
2036
+ const ctaUrl = getSlotValue(slots, t, "cta_url") || "#";
2037
+ const highlighted = getSlotValue(slots, t, "highlighted") === "yes";
2038
+ const borderColor = highlighted ? "var(--pw-brand-primary)" : "rgba(255,255,255,.08)";
2039
+ return `<div style="border-color:${borderColor};border-width:${highlighted ? "2px" : "var(--pw-border-width)"}">
2040
+ <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>
2041
+ <div style="display:flex;align-items:baseline;gap:2px;margin-bottom:var(--pw-spacing-md)">
2042
+ <span style="font-size:2rem;font-weight:700;font-family:var(--pw-font-heading)">${escHtml(price)}</span>
2043
+ <span style="font-size:.875rem;color:var(--pw-brand-muted)">${escHtml(period)}</span>
2044
+ </div>
2045
+ <ul style="margin:0 0 var(--pw-spacing-md);padding:0;list-style:none;space-y:var(--pw-spacing-xs)">
2046
+ ${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")}
2047
+ </ul>
2048
+ <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>
2049
+ </div>`;
2050
+ }
2051
+ function renderChangelogRowHtml(slots, t) {
2052
+ const version = getSlotValue(slots, t, "version");
2053
+ const date = getSlotValue(slots, t, "date");
2054
+ const label = getSlotValue(slots, t, "label");
2055
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
2056
+ const labelColor = { feat: "#22c55e", fix: "#f59e0b", docs: "#60a5fa", chore: "#94a3b8" };
2057
+ const lc = labelColor[label] ?? "var(--pw-brand-muted)";
2058
+ return `<div style="display:flex;align-items:baseline;gap:var(--pw-spacing-sm);margin-bottom:var(--pw-spacing-sm);flex-wrap:wrap">
2059
+ <span style="font-family:var(--pw-font-mono);font-size:.875rem;font-weight:600;color:var(--pw-brand-primary)">${escHtml(version)}</span>
2060
+ ${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>` : ""}
2061
+ <span style="font-size:.75rem;color:var(--pw-brand-muted);font-family:var(--pw-font-mono)">${escHtml(date)}</span>
2062
+ </div>
2063
+ <ul style="margin:0;padding:0 0 0 var(--pw-spacing-md);list-style:disc;color:var(--pw-brand-muted)">
2064
+ ${changes.map((c) => ` <li style="font-size:.875rem;line-height:1.5">${escHtml(c)}</li>`).join("\n")}
2065
+ </ul>`;
2066
+ }
2067
+ function renderSocialProofHtml(slots, t) {
2068
+ const caption = getSlotValue(slots, t, "caption");
2069
+ const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
2070
+ const divider = getSlotValue(slots, t, "show_divider") === "yes";
2071
+ 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>` : ""}
2072
+ ${divider ? `<hr style="border:none;border-top:1px solid rgba(255,255,255,.08);margin:var(--pw-spacing-sm) 0" />` : ""}
2073
+ <div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:var(--pw-spacing-lg)">
2074
+ ${logos.map((l) => ` <span style="font-weight:600;font-size:.875rem;color:var(--pw-brand-muted);letter-spacing:-.01em">${escHtml(l)}</span>`).join("\n")}
2075
+ </div>`;
2076
+ }
2077
+ var HTML_BODY_FN = {
2078
+ "stat-card": renderStatCardHtml,
2079
+ "feature-grid": renderFeatureGridHtml,
2080
+ "testimonial": renderTestimonialHtml,
2081
+ "cta-banner": renderCtaBannerHtml,
2082
+ "metric-badge": renderMetricBadgeHtml,
2083
+ "pricing-tier": renderPricingTierHtml,
2084
+ "changelog-row": renderChangelogRowHtml,
2085
+ "social-proof": renderSocialProofHtml
2086
+ };
2087
+ function renderStatCardMd(slots, t) {
2088
+ const value = getSlotValue(slots, t, "value");
2089
+ const label = getSlotValue(slots, t, "label");
2090
+ const delta = getSlotValue(slots, t, "delta");
2091
+ const dir = getSlotValue(slots, t, "delta_direction");
2092
+ const unit = getSlotValue(slots, t, "unit");
2093
+ const arrow = dir === "up" ? "\u2191" : dir === "down" ? "\u2193" : "";
2094
+ return `**${value}${unit ? " " + unit : ""}** \u2014 ${label}${delta ? ` ${arrow} ${delta}` : ""}`;
2095
+ }
2096
+ function renderFeatureGridMd(slots, t) {
2097
+ const title = getSlotValue(slots, t, "title");
2098
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2099
+ const icon = getSlotValue(slots, t, "icon") || "\u2713";
2100
+ return `${title ? `## ${title}
2101
+
2102
+ ` : ""}${features.map((f) => `- ${icon} ${f}`).join("\n")}`;
2103
+ }
2104
+ function renderTestimonialMd(slots, t) {
2105
+ const quote = getSlotValue(slots, t, "quote");
2106
+ const author = getSlotValue(slots, t, "author");
2107
+ const role = getSlotValue(slots, t, "role");
2108
+ const company = getSlotValue(slots, t, "company");
2109
+ const attribution = [author, role, company].filter(Boolean).join(", ");
2110
+ return `> "${quote}"
2111
+ >
2112
+ > \u2014 ${attribution}`;
2113
+ }
2114
+ function renderCtaBannerMd(slots, t) {
2115
+ const heading = getSlotValue(slots, t, "heading");
2116
+ const subtitle = getSlotValue(slots, t, "subtitle");
2117
+ const btnText = getSlotValue(slots, t, "button_text");
2118
+ const btnUrl = getSlotValue(slots, t, "button_url") || "#";
2119
+ return `## ${heading}
2120
+
2121
+ ${subtitle ? subtitle + "\n\n" : ""}[${btnText}](${btnUrl})`;
2122
+ }
2123
+ function renderMetricBadgeMd(slots, t) {
2124
+ const name = getSlotValue(slots, t, "metric_name");
2125
+ const value = getSlotValue(slots, t, "value");
2126
+ const unit = getSlotValue(slots, t, "unit");
2127
+ return `**${name}**: \`${value}${unit}\``;
2128
+ }
2129
+ function renderPricingTierMd(slots, t) {
2130
+ const plan = getSlotValue(slots, t, "plan_name");
2131
+ const price = getSlotValue(slots, t, "price");
2132
+ const period = getSlotValue(slots, t, "period");
2133
+ const features = getSlotValue(slots, t, "features").split("\n").filter(Boolean);
2134
+ const cta = getSlotValue(slots, t, "cta_text");
2135
+ const ctaUrl = getSlotValue(slots, t, "cta_url") || "#";
2136
+ return `### ${plan} \u2014 ${price}${period}
2137
+
2138
+ ${features.map((f) => `- \u2713 ${f}`).join("\n")}
2139
+
2140
+ [${cta}](${ctaUrl})`;
2141
+ }
2142
+ function renderChangelogRowMd(slots, t) {
2143
+ const version = getSlotValue(slots, t, "version");
2144
+ const date = getSlotValue(slots, t, "date");
2145
+ const label = getSlotValue(slots, t, "label");
2146
+ const changes = getSlotValue(slots, t, "changes").split("\n").filter(Boolean);
2147
+ return `### ${version}${label ? ` \`${label}\`` : ""} \u2014 ${date}
2148
+
2149
+ ${changes.map((c) => `- ${c}`).join("\n")}`;
2150
+ }
2151
+ function renderSocialProofMd(slots, t) {
2152
+ const caption = getSlotValue(slots, t, "caption");
2153
+ const logos = getSlotValue(slots, t, "logos").split("\n").filter(Boolean);
2154
+ return `${caption ? caption + "\n\n" : ""}${logos.join(" \xB7 ")}`;
2155
+ }
2156
+ var MD_FN = {
2157
+ "stat-card": renderStatCardMd,
2158
+ "feature-grid": renderFeatureGridMd,
2159
+ "testimonial": renderTestimonialMd,
2160
+ "cta-banner": renderCtaBannerMd,
2161
+ "metric-badge": renderMetricBadgeMd,
2162
+ "pricing-tier": renderPricingTierMd,
2163
+ "changelog-row": renderChangelogRowMd,
2164
+ "social-proof": renderSocialProofMd
2165
+ };
2166
+ function toReactStyle(vars) {
2167
+ return "{\n" + Object.entries(vars).map(([k, v]) => ` "${k}": "${v.replace(/"/g, '\\"')}"`).join(",\n") + "\n }";
2168
+ }
2169
+ function renderReact(template, slots, brand, style) {
2170
+ const allVars = { ...brandVars(brand), ...styleVars(style) };
2171
+ const componentName = template.id.split("-").map((p) => p[0].toUpperCase() + p.slice(1)).join("");
2172
+ const slotDecls = template.slots.map((s) => {
2173
+ const val = getSlotValue(slots, template, s.id);
2174
+ if (s.type === "number") return ` const ${s.id.replace(/-/g, "_")} = ${Number(val) || 0};`;
2175
+ return ` const ${s.id.replace(/-/g, "_")} = ${JSON.stringify(val)};`;
2176
+ }).join("\n");
2177
+ const htmlBody = (HTML_BODY_FN[template.id]?.(slots, template) ?? "").replace(/`/g, "\\`");
2178
+ return `// forge0x2B \u2014 ${template.name} widget
2179
+ // Generated by forgesmith widget renderer
2180
+
2181
+ const styleVars = ${toReactStyle(allVars)};
2182
+
2183
+ export default function ${componentName}() {
2184
+ ${slotDecls}
2185
+
2186
+ return (
2187
+ <div
2188
+ className="pw-widget pw-${template.id}"
2189
+ style={styleVars}
2190
+ dangerouslySetInnerHTML={{ __html: \`${htmlBody}\` }}
2191
+ />
2192
+ );
2193
+ }
2194
+ `;
2195
+ }
2196
+ function renderWidget(input) {
2197
+ const { template, slots, brandKit, format } = input;
2198
+ const style = !input.style ? STYLE_PRESET_DEFAULT : typeof input.style === "string" ? getStyleById(input.style) : input.style;
2199
+ if (format === "markdown") {
2200
+ const fn = MD_FN[template.id];
2201
+ return fn ? fn(slots, template) : `<!-- ${template.name} -->`;
2202
+ }
2203
+ if (format === "react") {
2204
+ return renderReact(template, slots, brandKit ?? null, style);
2205
+ }
2206
+ const bodyFn = HTML_BODY_FN[template.id];
2207
+ const body = bodyFn ? bodyFn(slots, template) : "";
2208
+ const allVars = { ...brandVars(brandKit ?? null), ...styleVars(style) };
2209
+ const inlineStyle = buildInlineStyle(allVars);
2210
+ const customCss = style.customCss ?? "";
2211
+ const override = style.templateOverrides?.[template.id]?.html;
2212
+ const finalBody = override ?? body;
2213
+ return `<div class="pw-widget pw-${template.id}" style="${inlineStyle}">
2214
+ <style>.pw-widget{${baseWidgetStyle().replace(/^\.pw-widget\{/, "").replace(/\}$/, "")}${customCss ? " " + customCss : ""}</style>
2215
+ ${finalBody}
2216
+ </div>`;
2217
+ }
2218
+
2219
+ // src/forge/fixtures/audiences.ts
2220
+ var FORGE_AUDIENCES = [
2221
+ {
2222
+ id: asAudienceId("heise_technical_editorial"),
2223
+ label: "Heise \u2014 Technische Redaktion",
2224
+ tonality: "sachlich-tief",
2225
+ reading_level: "expert",
2226
+ channel_hints: ["heise"],
2227
+ description: "Fachredaktion mit tiefem technischem Hintergrund; erwartet Belege, Architekturbezug und keine Marketingsprache."
2228
+ },
2229
+ {
2230
+ id: asAudienceId("connect_consumer_tech"),
2231
+ label: "Connect \u2014 Consumer Tech",
2232
+ tonality: "sachlich-zug\xE4nglich",
2233
+ reading_level: "professional",
2234
+ channel_hints: ["connect"],
2235
+ description: "Consumer-Tech-Redaktion mit Fokus auf Alltagstauglichkeit und Vergleichbarkeit gegen\xFCber Marktalternativen."
2236
+ },
2237
+ {
2238
+ id: asAudienceId("investor_seed"),
2239
+ label: "Investor \u2014 Seed",
2240
+ tonality: "ambitioniert-glaubw\xFCrdig",
2241
+ reading_level: "professional",
2242
+ channel_hints: ["internal", "deck", "email"],
2243
+ description: "Seed-Stage Investor; erwartet These, Marktverst\xE4ndnis und glaubw\xFCrdige Traktion in wenigen Abs\xE4tzen."
2244
+ },
2245
+ {
2246
+ id: asAudienceId("investor_growth"),
2247
+ label: "Investor \u2014 Growth",
2248
+ tonality: "n\xFCchtern-direkt",
2249
+ reading_level: "professional",
2250
+ channel_hints: ["internal", "deck", "email"],
2251
+ description: "Growth-Stage Investor; erwartet Kennzahlen, Defensibility und operative Hebel."
2252
+ },
2253
+ {
2254
+ id: asAudienceId("founder_peer"),
2255
+ label: "Founder Peer",
2256
+ tonality: "sachlich-zug\xE4nglich",
2257
+ reading_level: "professional",
2258
+ channel_hints: ["linkedin", "internal"],
2259
+ description: "Gr\xFCnderkollege auf vergleichbarem Reifegrad; Austausch ohne Pitch-Ton."
2260
+ },
2261
+ {
2262
+ id: asAudienceId("b2b_buyer"),
2263
+ label: "B2B Buyer",
2264
+ tonality: "n\xFCchtern-direkt",
2265
+ reading_level: "professional",
2266
+ channel_hints: ["landing", "onepager", "email"],
2267
+ description: "Entscheider in einem Zielunternehmen; bewertet Nutzen, Integration und Risiko."
2268
+ },
2269
+ {
2270
+ id: asAudienceId("end_customer_friendly"),
2271
+ label: "Endkunde \u2014 freundlich",
2272
+ tonality: "warm-zug\xE4nglich",
2273
+ reading_level: "general",
2274
+ channel_hints: ["landing", "instagram", "email"],
2275
+ description: "Endnutzerin oder Endnutzer ohne Vorwissen; sucht direkten Nutzen und Vertrauen."
2276
+ },
2277
+ {
2278
+ id: asAudienceId("partner_business"),
2279
+ label: "Gesch\xE4ftspartner",
2280
+ tonality: "sachlich-zug\xE4nglich",
2281
+ reading_level: "professional",
2282
+ channel_hints: ["email", "onepager", "deck"],
2283
+ description: "Vertriebs- oder Integrationspartner; bewertet Mehrwert f\xFCr die eigene Wertsch\xF6pfung."
2284
+ },
2285
+ {
2286
+ id: asAudienceId("linkedin_founder_post"),
2287
+ label: "LinkedIn \u2014 Founder Post",
2288
+ tonality: "ambitioniert-glaubw\xFCrdig",
2289
+ reading_level: "professional",
2290
+ channel_hints: ["linkedin"],
2291
+ description: "LinkedIn-Publikum eines Gr\xFCnderprofils; erwartet pers\xF6nlichen Ton mit Substanz."
2292
+ },
2293
+ {
2294
+ id: asAudienceId("linkedin_product_post"),
2295
+ label: "LinkedIn \u2014 Product Post",
2296
+ tonality: "sachlich-zug\xE4nglich",
2297
+ reading_level: "professional",
2298
+ channel_hints: ["linkedin"],
2299
+ description: "LinkedIn-Publikum eines Produktprofils; Fokus auf Funktionalit\xE4t und Anwendungsfall."
2300
+ },
2301
+ {
2302
+ id: asAudienceId("instagram_carousel_general"),
2303
+ label: "Instagram \u2014 Carousel allgemein",
2304
+ tonality: "warm-zug\xE4nglich",
2305
+ reading_level: "general",
2306
+ channel_hints: ["instagram"],
2307
+ description: "Allgemeines Instagram-Publikum; visuelle Logik, kurze Hooks, freundlicher Ton."
2308
+ },
2309
+ {
2310
+ id: asAudienceId("friend_explainer"),
2311
+ label: "Freund \u2014 Erkl\xE4rung",
2312
+ tonality: "warm-zug\xE4nglich",
2313
+ reading_level: "lay",
2314
+ channel_hints: ["internal", "email"],
2315
+ description: "Eine fachfremde, nahestehende Person; sucht Verst\xE4ndnis ohne Fachbegriffe."
2316
+ }
2317
+ ];
2318
+
2319
+ // src/forge/fixtures/templates.ts
2320
+ var FORGE_TEMPLATES = [
2321
+ {
2322
+ id: "heise_long_article",
2323
+ channel: "heise",
2324
+ audience_id: asAudienceId("heise_technical_editorial"),
2325
+ intent: "inform",
2326
+ format: "long_article",
2327
+ modality: "text",
2328
+ tonality: "sachlich-tief",
2329
+ schema_json: {
2330
+ sections: ["lede", "context", "architecture", "evidence", "outlook"]
2331
+ },
2332
+ asset_slots: []
2333
+ },
2334
+ {
2335
+ id: "founder_seed_pitch",
2336
+ channel: "internal",
2337
+ audience_id: asAudienceId("investor_seed"),
2338
+ intent: "pitch",
2339
+ format: "pitch_paragraphs",
2340
+ modality: "text",
2341
+ tonality: "ambitioniert-glaubw\xFCrdig",
2342
+ schema_json: { sections: ["thesis", "market", "wedge", "traction", "ask"] },
2343
+ asset_slots: []
2344
+ },
2345
+ {
2346
+ id: "linkedin_founder_post",
2347
+ channel: "linkedin",
2348
+ audience_id: asAudienceId("linkedin_founder_post"),
2349
+ intent: "announce",
2350
+ format: "short_post",
2351
+ modality: "text_with_visual_brief",
2352
+ tonality: "ambitioniert-glaubw\xFCrdig",
2353
+ schema_json: { sections: ["hook", "body", "cta"] },
2354
+ asset_slots: [{ kind: "image_brief", required: true }]
2355
+ },
2356
+ {
2357
+ id: "linkedin_product_carousel",
2358
+ channel: "linkedin",
2359
+ audience_id: asAudienceId("linkedin_product_post"),
2360
+ intent: "educate",
2361
+ format: "carousel_sequence",
2362
+ modality: "text_with_visual_brief",
2363
+ tonality: "sachlich-zug\xE4nglich",
2364
+ schema_json: { slides: { min: 5, max: 8 } },
2365
+ asset_slots: [{ kind: "image_brief", required: true, per: "slide" }]
2366
+ },
2367
+ {
2368
+ id: "instagram_carousel_general",
2369
+ channel: "instagram",
2370
+ audience_id: asAudienceId("instagram_carousel_general"),
2371
+ intent: "celebrate_release",
2372
+ format: "carousel_sequence",
2373
+ modality: "text_with_visual_brief",
2374
+ tonality: "warm-zug\xE4nglich",
2375
+ schema_json: { slides: { min: 4, max: 7 } },
2376
+ asset_slots: [{ kind: "image_brief", required: true, per: "slide" }]
2377
+ },
2378
+ {
2379
+ id: "landing_section",
2380
+ channel: "landing",
2381
+ audience_id: asAudienceId("b2b_buyer"),
2382
+ intent: "convince",
2383
+ format: "landing_block",
2384
+ modality: "text",
2385
+ tonality: "n\xFCchtern-direkt",
2386
+ schema_json: { sections: ["headline", "subhead", "bullets", "cta"] },
2387
+ asset_slots: []
2388
+ },
2389
+ {
2390
+ id: "friend_explainer_short",
2391
+ channel: "internal",
2392
+ audience_id: asAudienceId("friend_explainer"),
2393
+ intent: "explain_to_layperson",
2394
+ format: "short_post",
2395
+ modality: "text",
2396
+ tonality: "warm-zug\xE4nglich",
2397
+ schema_json: {
2398
+ sections: ["analogy", "what_it_does", "why_it_matters"]
2399
+ },
2400
+ asset_slots: []
2401
+ },
2402
+ {
2403
+ id: "connect_consumer_review",
2404
+ channel: "connect",
2405
+ audience_id: asAudienceId("connect_consumer_tech"),
2406
+ intent: "inform",
2407
+ format: "long_article",
2408
+ modality: "text",
2409
+ tonality: "sachlich-zug\xE4nglich",
2410
+ schema_json: {
2411
+ sections: ["intro", "everyday_use", "comparison", "verdict"]
2412
+ },
2413
+ asset_slots: []
2414
+ }
2415
+ ];
2416
+
2417
+ // src/forge/secrets.ts
2418
+ var SECRET_PATTERNS = [
2419
+ /sk-ant-api03-[A-Za-z0-9_-]{80,}/,
2420
+ /sk-proj-[A-Za-z0-9_-]{40,}/,
2421
+ /AKIA[0-9A-Z]{16}/,
2422
+ /(?:password|secret|token|api_key)\s*[:=]\s*["']?[A-Za-z0-9_\-+/]{16,}/i
2423
+ ];
2424
+ function scanForSecrets(text) {
2425
+ return SECRET_PATTERNS.some((re) => re.test(text));
2426
+ }
2427
+
2428
+ // src/forge/prismContext.data.ts
2429
+ var PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT = {
2430
+ id: "release-announcement",
2431
+ name: "Release Announcement",
2432
+ description: "Announce what changed based on recent blueprint activity.",
2433
+ defaultFocus: "release",
2434
+ 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.",
2435
+ suggestedChannels: ["tweet", "linkedin", "newsletter"]
2436
+ };
2437
+ var PRISM_TEMPLATE_SHIPPING_DIGEST = {
2438
+ id: "weekly-shipping-digest",
2439
+ name: "Weekly Shipping Digest",
2440
+ description: "What we shipped this week \u2014 grounded in real zone activity.",
2441
+ defaultFocus: "changelog",
2442
+ 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.",
2443
+ suggestedChannels: ["newsletter", "slack", "linkedin"]
2444
+ };
2445
+ var PRISM_TEMPLATE_ZONE_DEEPDIVE = {
2446
+ id: "zone-deepdive",
2447
+ name: "Zone Deep-Dive",
2448
+ description: "Deep-dive explanation of a specific zone for developers.",
2449
+ defaultFocus: "deepdive",
2450
+ 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.",
2451
+ suggestedChannels: ["blog", "hn"],
2452
+ requiresZone: true
2453
+ };
2454
+ var PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW = {
2455
+ id: "architecture-overview",
2456
+ name: "Architecture Overview",
2457
+ description: "Full codebase structure \u2014 onboarding or blog post.",
2458
+ defaultFocus: "deepdive",
2459
+ 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.",
2460
+ suggestedChannels: ["blog", "newsletter"]
2461
+ };
2462
+ var PRISM_TEMPLATE_REFACTOR_RATIONALE = {
2463
+ id: "refactor-rationale",
2464
+ name: "Refactor Rationale",
2465
+ description: "Explain why changes were made \u2014 dependency + coupling context.",
2466
+ defaultFocus: "release",
2467
+ 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.",
2468
+ suggestedChannels: ["blog", "email", "hn"]
2469
+ };
2470
+ var PRISM_TEMPLATES = [
2471
+ PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT,
2472
+ PRISM_TEMPLATE_SHIPPING_DIGEST,
2473
+ PRISM_TEMPLATE_ZONE_DEEPDIVE,
2474
+ PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW,
2475
+ PRISM_TEMPLATE_REFACTOR_RATIONALE
2476
+ ];
2477
+ function getPrismTemplate(id) {
2478
+ return PRISM_TEMPLATES.find((t) => t.id === id);
2479
+ }
2480
+
2481
+ // src/forge/channel.ts
2482
+ var DISPATCH_CHANNEL_TWEET = {
2483
+ id: "tweet",
2484
+ name: "X / Twitter",
2485
+ kind: "tweet",
2486
+ maxLength: 280,
2487
+ 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."
2488
+ };
2489
+ var DISPATCH_CHANNEL_LINKEDIN = {
2490
+ id: "linkedin",
2491
+ name: "LinkedIn",
2492
+ kind: "linkedin",
2493
+ maxLength: 1300,
2494
+ 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."
2495
+ };
2496
+ var DISPATCH_CHANNEL_BLOG = {
2497
+ id: "blog",
2498
+ name: "Blog Post",
2499
+ kind: "blog",
2500
+ maxLength: 3e3,
2501
+ 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."
2502
+ };
2503
+ var DISPATCH_CHANNEL_NEWSLETTER = {
2504
+ id: "newsletter",
2505
+ name: "Newsletter",
2506
+ kind: "newsletter",
2507
+ maxLength: 1800,
2508
+ 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."
2509
+ };
2510
+ var DISPATCH_CHANNEL_HN = {
2511
+ id: "hn",
2512
+ name: "Hacker News",
2513
+ kind: "hn",
2514
+ maxLength: 800,
2515
+ 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."
2516
+ };
2517
+ var DISPATCH_CHANNEL_INSTAGRAM = {
2518
+ id: "instagram",
2519
+ name: "Instagram",
2520
+ kind: "instagram",
2521
+ maxLength: 2200,
2522
+ 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."
2523
+ };
2524
+ var DISPATCH_CHANNEL_REDDIT = {
2525
+ id: "reddit",
2526
+ name: "Reddit",
2527
+ kind: "reddit",
2528
+ maxLength: 1e3,
2529
+ 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."
2530
+ };
2531
+ var DISPATCH_CHANNEL_EMAIL = {
2532
+ id: "email",
2533
+ name: "Email",
2534
+ kind: "email",
2535
+ maxLength: 1500,
2536
+ 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."
2537
+ };
2538
+ var DISPATCH_CHANNEL_SLACK = {
2539
+ id: "slack",
2540
+ name: "Slack",
2541
+ kind: "slack",
2542
+ maxLength: 600,
2543
+ 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."
2544
+ };
2545
+ var BUNDLED_DISPATCH_CHANNELS = [
2546
+ DISPATCH_CHANNEL_TWEET,
2547
+ DISPATCH_CHANNEL_LINKEDIN,
2548
+ DISPATCH_CHANNEL_BLOG,
2549
+ DISPATCH_CHANNEL_NEWSLETTER,
2550
+ DISPATCH_CHANNEL_HN,
2551
+ DISPATCH_CHANNEL_INSTAGRAM,
2552
+ DISPATCH_CHANNEL_REDDIT,
2553
+ DISPATCH_CHANNEL_EMAIL,
2554
+ DISPATCH_CHANNEL_SLACK
2555
+ ];
2556
+ function getDispatchChannel(id) {
2557
+ return BUNDLED_DISPATCH_CHANNELS.find((c) => c.id === id);
2558
+ }
2559
+
2560
+ // src/forge/dispatch.ts
2561
+ function buildSystemPrompt7(channel, audience, brand, blueprintContext, toneOffset) {
2562
+ const lines = [
2563
+ `You are a skilled content writer producing a ${channel.name} post.`,
2564
+ "",
2565
+ `Channel rules:`,
2566
+ `- Max length: ${channel.maxLength} characters (HARD LIMIT \u2014 do not exceed)`,
2567
+ `- ${channel.promptHints}`
2568
+ ];
2569
+ if (audience) {
2570
+ lines.push("", `Audience: ${audience.label}`);
2571
+ lines.push(`Tone: ${audience.tonality}`);
2572
+ lines.push(`Reading level: ${audience.reading_level}`);
2573
+ }
2574
+ if (toneOffset !== void 0 && toneOffset !== 0) {
2575
+ const adj = toneOffset > 0 ? "more formal and professional" : "more casual and conversational";
2576
+ lines.push(`Tone adjustment: ${adj} (offset ${toneOffset > 0 ? "+" : ""}${toneOffset})`);
2577
+ }
2578
+ if (brand) {
2579
+ lines.push("", `Brand voice: ${brand.voice.tone}, formality ${brand.voice.formality}/100`);
2580
+ if (brand.voice.vocabulary && brand.voice.vocabulary.length > 0) {
2581
+ lines.push(`Preferred vocabulary: ${brand.voice.vocabulary.slice(0, 8).join(", ")}`);
2582
+ }
2583
+ if (brand.voice.avoid && brand.voice.avoid.length > 0) {
2584
+ lines.push(`Avoid these words: ${brand.voice.avoid.slice(0, 6).join(", ")}`);
2585
+ }
2586
+ }
2587
+ if (blueprintContext) {
2588
+ lines.push("", "\u2500\u2500\u2500 CODEBASE CONTEXT (from prism blueprint) \u2500\u2500\u2500");
2589
+ lines.push(blueprintContext);
2590
+ lines.push(
2591
+ "You may reference specific metrics above (file counts, zones, churn) to make the post concrete and credible.",
2592
+ "Do not invent metrics that are not in the context."
2593
+ );
2594
+ }
2595
+ lines.push(
2596
+ "",
2597
+ "IMPORTANT: Return ONLY the post content \u2014 no preamble, no 'Here is your post:', no wrapper quotes.",
2598
+ "Stay strictly within the character limit."
2599
+ );
2600
+ return lines.join("\n");
2601
+ }
2602
+ function truncate(content, maxLength) {
2603
+ if (content.length <= maxLength) return { content, truncated: false };
2604
+ const cut = content.slice(0, maxLength);
2605
+ const lastSpace = cut.lastIndexOf(" ");
2606
+ const trimmed = lastSpace > maxLength * 0.8 ? cut.slice(0, lastSpace) : cut;
2607
+ return { content: trimmed.trimEnd(), truncated: true };
2608
+ }
2609
+ async function generateForChannel(ask, channel, provider, opts) {
2610
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2611
+ try {
2612
+ const system = buildSystemPrompt7(
2613
+ channel,
2614
+ opts.audience,
2615
+ opts.brand,
2616
+ opts.blueprintContext,
2617
+ opts.toneOffset
2618
+ );
2619
+ const resp = await provider.complete({
2620
+ systemPrompt: system,
2621
+ messages: [{ role: "user", content: ask }],
2622
+ maxTokens: Math.ceil(channel.maxLength / 3.5) + 100
2623
+ // rough token estimate + buffer
2624
+ });
2625
+ const raw = resp.content.trim();
2626
+ const { content, truncated } = truncate(raw, channel.maxLength);
2627
+ return {
2628
+ output: {
2629
+ channel_id: channel.id,
2630
+ channel_name: channel.name,
2631
+ kind: channel.kind,
2632
+ content,
2633
+ char_count: content.length,
2634
+ truncated,
2635
+ generated_at: now
2636
+ },
2637
+ inputTokens: resp.usedTokens,
2638
+ // approximation — most providers return total
2639
+ outputTokens: 0
2640
+ };
2641
+ } catch (err) {
2642
+ return {
2643
+ output: {
2644
+ channel_id: channel.id,
2645
+ channel_name: channel.name,
2646
+ kind: channel.kind,
2647
+ content: "",
2648
+ char_count: 0,
2649
+ truncated: false,
2650
+ generated_at: now,
2651
+ error: err instanceof Error ? err.message : String(err)
2652
+ },
2653
+ inputTokens: 0,
2654
+ outputTokens: 0
2655
+ };
2656
+ }
2657
+ }
2658
+ async function orchestrateDispatch(input, provider) {
2659
+ const { ask, channels, audience, brand, blueprintContext, toneOverrides = {} } = input;
2660
+ const results = await Promise.all(
2661
+ channels.map(
2662
+ (ch) => generateForChannel(ask, ch, provider, {
2663
+ audience,
2664
+ brand,
2665
+ blueprintContext,
2666
+ toneOffset: toneOverrides[ch.id]
2667
+ })
2668
+ )
2669
+ );
2670
+ const outputs = results.map((r) => r.output);
2671
+ const totalInput = results.reduce((acc, r) => acc + r.inputTokens, 0);
2672
+ const totalOutput = results.reduce((acc, r) => acc + r.outputTokens, 0);
2673
+ return {
2674
+ outputs,
2675
+ usage: {
2676
+ total_input: totalInput,
2677
+ total_output: totalOutput,
2678
+ channel_count: channels.length
2679
+ }
2680
+ };
2681
+ }
2682
+
2683
+ // src/forge/assetVersioning.ts
2684
+ function uuid() {
2685
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2686
+ const r = Math.random() * 16 | 0;
2687
+ return (c === "x" ? r : r & 3 | 8).toString(16);
2688
+ });
2689
+ }
2690
+ function computeDiff(contentA, contentB) {
2691
+ const linesA = contentA.split("\n");
2692
+ const linesB = contentB.split("\n");
2693
+ const m = linesA.length;
2694
+ const n = linesB.length;
2695
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
2696
+ for (let i2 = 1; i2 <= m; i2++) {
2697
+ for (let j2 = 1; j2 <= n; j2++) {
2698
+ if (linesA[i2 - 1] === linesB[j2 - 1]) {
2699
+ dp[i2][j2] = dp[i2 - 1][j2 - 1] + 1;
2700
+ } else {
2701
+ dp[i2][j2] = Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
2702
+ }
2703
+ }
2704
+ }
2705
+ const entries = [];
2706
+ let i = m, j = n;
2707
+ while (i > 0 || j > 0) {
2708
+ if (i > 0 && j > 0 && linesA[i - 1] === linesB[j - 1]) {
2709
+ entries.push({ type: "unchanged", line: linesA[i - 1], lineNo: i });
2710
+ i--;
2711
+ j--;
2712
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
2713
+ entries.push({ type: "added", line: linesB[j - 1] });
2714
+ j--;
2715
+ } else {
2716
+ entries.push({ type: "removed", line: linesA[i - 1], lineNo: i });
2717
+ i--;
2718
+ }
2719
+ }
2720
+ entries.reverse();
2721
+ const addedCount = entries.filter((e) => e.type === "added").length;
2722
+ const removedCount = entries.filter((e) => e.type === "removed").length;
2723
+ return { entries, addedCount, removedCount };
2724
+ }
2725
+ function buildVersion(assetId, versionNumber, content, message, meta) {
2726
+ return {
2727
+ id: uuid(),
2728
+ assetId,
2729
+ versionNumber,
2730
+ content,
2731
+ message,
2732
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2733
+ ...meta
2734
+ };
2735
+ }
2736
+ function buildRevertVersion(assetId, versionNumber, source) {
2737
+ return {
2738
+ id: uuid(),
2739
+ assetId,
2740
+ versionNumber,
2741
+ content: source.content,
2742
+ message: `Reverted to v${source.versionNumber}`,
2743
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2744
+ brandKitId: source.brandKitId,
2745
+ styleId: source.styleId,
2746
+ blueprintFocus: source.blueprintFocus,
2747
+ fromPrism: source.fromPrism,
2748
+ revertedFrom: source.versionNumber
2749
+ };
2750
+ }
2751
+ function nextVersionNumber(versions) {
2752
+ if (versions.length === 0) return 1;
2753
+ return Math.max(...versions.map((v) => v.versionNumber)) + 1;
2754
+ }
2755
+
2756
+ // src/forge/refineAsset.ts
2757
+ function buildSystemPrompt8(input) {
2758
+ const parts = [
2759
+ `You are a content refinement assistant for a developer-focused content tool (forge0x2B).`,
2760
+ `You are refining an existing ${input.assetType.replace(/-/g, " ")} asset.`,
2761
+ ``,
2762
+ `TASK: Apply the user's requested change to the asset content below. Return two things:`,
2763
+ `1. REFINED_CONTENT: The complete revised content (not just the changed parts)`,
2764
+ `2. REPLY: A brief explanation of what you changed and why (1-3 sentences)`,
2765
+ ``,
2766
+ `FORMAT YOUR RESPONSE EXACTLY LIKE THIS (no extra text outside these markers):`,
2767
+ `<REFINED_CONTENT>`,
2768
+ `[the full revised content here]`,
2769
+ `</REFINED_CONTENT>`,
2770
+ `<REPLY>`,
2771
+ `[your brief explanation here]`,
2772
+ `</REPLY>`
2773
+ ];
2774
+ if (input.brandName || input.brandVoice) {
2775
+ parts.push(``, `Brand context:`);
2776
+ if (input.brandName) parts.push(` Name: ${input.brandName}`);
2777
+ if (input.brandVoice) parts.push(` Voice: ${input.brandVoice}`);
2778
+ }
2779
+ if (input.styleHint) {
2780
+ parts.push(` Style: ${input.styleHint}`);
2781
+ }
2782
+ if (input.blueprintContext) {
2783
+ parts.push(``, `Blueprint context (codebase facts to reference):`, input.blueprintContext);
2784
+ }
2785
+ parts.push(``, `CURRENT CONTENT:`, `---`, input.currentContent, `---`);
2786
+ return parts.join("\n");
2787
+ }
2788
+ function parseRefinedResponse(raw) {
2789
+ const contentMatch = raw.match(/<REFINED_CONTENT>([\s\S]*?)<\/REFINED_CONTENT>/);
2790
+ const replyMatch = raw.match(/<REPLY>([\s\S]*?)<\/REPLY>/);
2791
+ const newContent = contentMatch ? contentMatch[1].trim() : raw.trim();
2792
+ const llmReply = replyMatch ? replyMatch[1].trim() : "Content refined as requested.";
2793
+ return { newContent, llmReply };
2794
+ }
2795
+ async function refineAsset(input, provider) {
2796
+ const systemPrompt = buildSystemPrompt8(input);
2797
+ if (scanForSecrets(systemPrompt)) {
2798
+ throw new Error("refineAsset: secret pattern detected in asset content. Refusing to send to LLM.");
2799
+ }
2800
+ if (scanForSecrets(input.userMessage)) {
2801
+ throw new Error("refineAsset: secret pattern detected in user message. Refusing to send to LLM.");
2802
+ }
2803
+ const response = await provider.complete({
2804
+ messages: [{ role: "user", content: input.userMessage }],
2805
+ systemPrompt,
2806
+ maxTokens: 2e3
2807
+ });
2808
+ const { newContent, llmReply } = parseRefinedResponse(response.content);
2809
+ return {
2810
+ newContent,
2811
+ llmReply,
2812
+ usedTokens: response.usedTokens
2813
+ };
2814
+ }
2815
+ var REFINE_SUGGESTIONS = [
2816
+ "Make it more concise",
2817
+ "Add more technical detail",
2818
+ "Make the tone more formal",
2819
+ "Make the tone more casual and conversational",
2820
+ "Add a strong opening hook",
2821
+ "Strengthen the call to action",
2822
+ "Break into shorter paragraphs",
2823
+ "Focus more on developer impact"
2824
+ ];
2825
+ function buildScheduledEntry(assetId, channelId, scheduledFor, contentPreview, metadata = {}) {
2826
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2827
+ return {
2828
+ id: crypto.randomUUID(),
2829
+ assetId,
2830
+ channelId,
2831
+ scheduledFor,
2832
+ status: "draft",
2833
+ contentPreview: contentPreview.slice(0, 60),
2834
+ metadata,
2835
+ createdAt: now,
2836
+ updatedAt: now
2837
+ };
2838
+ }
2839
+ function applyEntryPatch(entry, patch) {
2840
+ return {
2841
+ ...entry,
2842
+ ...patch,
2843
+ metadata: { ...entry.metadata, ...patch.metadata ?? {} },
2844
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2845
+ };
2846
+ }
2847
+ function entryInRange(entry, range) {
2848
+ const d = entry.scheduledFor.slice(0, 10);
2849
+ return d >= range.from && d <= range.to;
2850
+ }
2851
+ function next7DaysRange() {
2852
+ const from = /* @__PURE__ */ new Date();
2853
+ const to = /* @__PURE__ */ new Date();
2854
+ to.setDate(to.getDate() + 6);
2855
+ return {
2856
+ from: from.toISOString().slice(0, 10),
2857
+ to: to.toISOString().slice(0, 10)
2858
+ };
2859
+ }
2860
+ function cascadingScheduledFor(channelIds, startFrom = /* @__PURE__ */ new Date()) {
2861
+ const result = {};
2862
+ channelIds.forEach((id, i) => {
2863
+ const d = new Date(startFrom);
2864
+ d.setDate(d.getDate() + i);
2865
+ d.setHours(9, 0, 0, 0);
2866
+ result[id] = d.toISOString();
2867
+ });
2868
+ return result;
2869
+ }
2870
+
2871
+ // src/forge/exporters.ts
2872
+ function csvEscape(value) {
2873
+ if (value.includes('"') || value.includes(",") || value.includes("\n") || value.includes("\r")) {
2874
+ return `"${value.replace(/"/g, '""')}"`;
2875
+ }
2876
+ return value;
2877
+ }
2878
+ function csvRow(cells) {
2879
+ return cells.map(csvEscape).join(",");
2880
+ }
2881
+ function exportToBufferCsv(entries) {
2882
+ const header = csvRow(["Date", "Time", "Content", "Link", "Image"]);
2883
+ const rows = entries.map((e) => {
2884
+ const d = new Date(e.scheduledFor);
2885
+ const pad = (n) => String(n).padStart(2, "0");
2886
+ const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
2887
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
2888
+ return csvRow([date, time, e.contentPreview, "", ""]);
2889
+ });
2890
+ return [header, ...rows].join("\r\n");
2891
+ }
2892
+ function exportToHypefuryCsv(entries) {
2893
+ const header = csvRow(["scheduled_at", "content", "tweet_type"]);
2894
+ const rows = entries.map(
2895
+ (e) => csvRow([e.scheduledFor, e.contentPreview, "tweet"])
2896
+ );
2897
+ return [header, ...rows].join("\r\n");
2898
+ }
2899
+ function icsDate(iso) {
2900
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}/, "").replace("Z", "Z");
2901
+ }
2902
+ function icsEscape(value) {
2903
+ return value.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n");
2904
+ }
2905
+ function foldLine(line) {
2906
+ const parts = [];
2907
+ while (line.length > 75) {
2908
+ parts.push(line.slice(0, 75));
2909
+ line = " " + line.slice(75);
2910
+ }
2911
+ parts.push(line);
2912
+ return parts.join("\r\n");
2913
+ }
2914
+ function exportToICalendar(entries) {
2915
+ const now = icsDate((/* @__PURE__ */ new Date()).toISOString());
2916
+ const events = entries.map((e) => {
2917
+ const dtStart = icsDate(e.scheduledFor);
2918
+ const end = new Date(new Date(e.scheduledFor).getTime() + 30 * 60 * 1e3);
2919
+ const dtEnd = icsDate(end.toISOString());
2920
+ const summary = icsEscape(`[${e.channelId}] ${e.contentPreview}`);
2921
+ const description = icsEscape(
2922
+ `Asset: ${e.assetId} \xB7 Channel: ${e.channelId} \xB7 Status: ${e.status}`
2923
+ );
2924
+ return [
2925
+ "BEGIN:VEVENT",
2926
+ foldLine(`UID:${e.id}@forge0x2B`),
2927
+ `DTSTAMP:${now}`,
2928
+ `DTSTART:${dtStart}`,
2929
+ `DTEND:${dtEnd}`,
2930
+ foldLine(`SUMMARY:${summary}`),
2931
+ foldLine(`DESCRIPTION:${description}`),
2932
+ `STATUS:${e.status === "queued" ? "CONFIRMED" : "TENTATIVE"}`,
2933
+ "END:VEVENT"
2934
+ ].join("\r\n");
2935
+ });
2936
+ return [
2937
+ "BEGIN:VCALENDAR",
2938
+ "VERSION:2.0",
2939
+ "PRODID:-//forge0x2B//Scheduler//EN",
2940
+ "CALSCALE:GREGORIAN",
2941
+ "METHOD:PUBLISH",
2942
+ ...events,
2943
+ "END:VCALENDAR"
2944
+ ].join("\r\n");
2945
+ }
2946
+ function previewExport(entries, format) {
2947
+ const subset = entries.slice(0, 5);
2948
+ switch (format) {
2949
+ case "buffer":
2950
+ return exportToBufferCsv(subset);
2951
+ case "hypefury":
2952
+ return exportToHypefuryCsv(subset);
2953
+ case "icalendar":
2954
+ return exportToICalendar(subset);
2955
+ }
2956
+ }
2957
+
2958
+ exports.ANIMATION_DURATION_PRESETS = ANIMATION_DURATION_PRESETS;
2959
+ exports.BRAND_CONTENT_SLOT_KEYS = BRAND_CONTENT_SLOT_KEYS;
2960
+ exports.BUNDLED_DISPATCH_CHANNELS = BUNDLED_DISPATCH_CHANNELS;
2961
+ exports.BUNDLED_STYLE_PRESETS = BUNDLED_STYLE_PRESETS;
2962
+ exports.BUNDLED_WIDGET_TEMPLATES = BUNDLED_WIDGET_TEMPLATES;
2963
+ exports.DEFAULT_ANIMATION_DURATION_SECONDS = DEFAULT_ANIMATION_DURATION_SECONDS;
2964
+ exports.DEFAULT_BRAND_KIT_FONTS = DEFAULT_BRAND_KIT_FONTS;
2965
+ exports.DEFAULT_BRAND_KIT_PALETTE = DEFAULT_BRAND_KIT_PALETTE;
2966
+ exports.DEFAULT_BRAND_KIT_VOICE = DEFAULT_BRAND_KIT_VOICE;
2967
+ exports.DISPATCH_CHANNEL_BLOG = DISPATCH_CHANNEL_BLOG;
2968
+ exports.DISPATCH_CHANNEL_EMAIL = DISPATCH_CHANNEL_EMAIL;
2969
+ exports.DISPATCH_CHANNEL_HN = DISPATCH_CHANNEL_HN;
2970
+ exports.DISPATCH_CHANNEL_INSTAGRAM = DISPATCH_CHANNEL_INSTAGRAM;
2971
+ exports.DISPATCH_CHANNEL_LINKEDIN = DISPATCH_CHANNEL_LINKEDIN;
2972
+ exports.DISPATCH_CHANNEL_NEWSLETTER = DISPATCH_CHANNEL_NEWSLETTER;
2973
+ exports.DISPATCH_CHANNEL_REDDIT = DISPATCH_CHANNEL_REDDIT;
2974
+ exports.DISPATCH_CHANNEL_SLACK = DISPATCH_CHANNEL_SLACK;
2975
+ exports.DISPATCH_CHANNEL_TWEET = DISPATCH_CHANNEL_TWEET;
2976
+ exports.FORGE_AUDIENCES = FORGE_AUDIENCES;
2977
+ exports.FORGE_BRAND_THEME_ID = FORGE_BRAND_THEME_ID;
2978
+ exports.FORGE_TEMPLATES = FORGE_TEMPLATES;
2979
+ exports.FREE_TIER_WIDGET_IDS = FREE_TIER_WIDGET_IDS;
2980
+ exports.MAX_ANIMATION_DURATION_SECONDS = MAX_ANIMATION_DURATION_SECONDS;
2981
+ exports.MIN_ANIMATION_DURATION_SECONDS = MIN_ANIMATION_DURATION_SECONDS;
2982
+ exports.PRISM_TEMPLATES = PRISM_TEMPLATES;
2983
+ exports.PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW = PRISM_TEMPLATE_ARCHITECTURE_OVERVIEW;
2984
+ exports.PRISM_TEMPLATE_REFACTOR_RATIONALE = PRISM_TEMPLATE_REFACTOR_RATIONALE;
2985
+ exports.PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT = PRISM_TEMPLATE_RELEASE_ANNOUNCEMENT;
2986
+ exports.PRISM_TEMPLATE_SHIPPING_DIGEST = PRISM_TEMPLATE_SHIPPING_DIGEST;
2987
+ exports.PRISM_TEMPLATE_ZONE_DEEPDIVE = PRISM_TEMPLATE_ZONE_DEEPDIVE;
2988
+ exports.REFINE_COUNTDOWN_THRESHOLD = REFINE_COUNTDOWN_THRESHOLD;
2989
+ exports.REFINE_SESSION_SOFT_CAP = REFINE_SESSION_SOFT_CAP;
2990
+ exports.REFINE_SUGGESTIONS = REFINE_SUGGESTIONS;
2991
+ exports.STYLE_PRESET_BRUTALIST = STYLE_PRESET_BRUTALIST;
2992
+ exports.STYLE_PRESET_DEFAULT = STYLE_PRESET_DEFAULT;
2993
+ exports.STYLE_PRESET_GLASSY = STYLE_PRESET_GLASSY;
2994
+ exports.STYLE_PRESET_MINIMAL = STYLE_PRESET_MINIMAL;
2995
+ exports.TEMPLATE_SCHEMA_EXAMPLES = TEMPLATE_SCHEMA_EXAMPLES;
2996
+ exports.TEMPLATE_SCHEMA_GENERIC = TEMPLATE_SCHEMA_GENERIC;
2997
+ exports.WIDGET_TEMPLATE_CHANGELOG_ROW = WIDGET_TEMPLATE_CHANGELOG_ROW;
2998
+ exports.WIDGET_TEMPLATE_CTA_BANNER = WIDGET_TEMPLATE_CTA_BANNER;
2999
+ exports.WIDGET_TEMPLATE_FEATURE_GRID = WIDGET_TEMPLATE_FEATURE_GRID;
3000
+ exports.WIDGET_TEMPLATE_METRIC_BADGE = WIDGET_TEMPLATE_METRIC_BADGE;
3001
+ exports.WIDGET_TEMPLATE_PRICING_TIER = WIDGET_TEMPLATE_PRICING_TIER;
3002
+ exports.WIDGET_TEMPLATE_SOCIAL_PROOF = WIDGET_TEMPLATE_SOCIAL_PROOF;
3003
+ exports.WIDGET_TEMPLATE_STAT_CARD = WIDGET_TEMPLATE_STAT_CARD;
3004
+ exports.WIDGET_TEMPLATE_TESTIMONIAL = WIDGET_TEMPLATE_TESTIMONIAL;
3005
+ exports.applyEntryPatch = applyEntryPatch;
3006
+ exports.asAudienceId = asAudienceId;
3007
+ exports.assembleBrandUrlExtractionPrompt = assembleBrandUrlExtractionPrompt;
3008
+ exports.assembleForgePrompt = assembleForgePrompt;
3009
+ exports.brandThemeConfigToEntry = brandThemeConfigToEntry;
3010
+ exports.buildRevertVersion = buildRevertVersion;
3011
+ exports.buildScheduledEntry = buildScheduledEntry;
3012
+ exports.buildVersion = buildVersion;
3013
+ exports.cascadingScheduledFor = cascadingScheduledFor;
3014
+ exports.clampAnimationDuration = clampAnimationDuration;
3015
+ exports.computeDiff = computeDiff;
3016
+ exports.defaultBrandKit = defaultBrandKit;
3017
+ exports.defaultValueForField = defaultValueForField;
3018
+ exports.distill = distill;
3019
+ exports.entryInRange = entryInRange;
3020
+ exports.exportToBufferCsv = exportToBufferCsv;
3021
+ exports.exportToHypefuryCsv = exportToHypefuryCsv;
3022
+ exports.exportToICalendar = exportToICalendar;
468
3023
  exports.generateArchitectureWalkthrough = generateArchitectureWalkthrough;
469
3024
  exports.generateAskDrivenAsset = generateAskDrivenAsset;
470
3025
  exports.generateChangesSince = generateChangesSince;
471
3026
  exports.generateOnboardingDoc = generateOnboardingDoc;
472
3027
  exports.generateRefactoringReport = generateRefactoringReport;
473
3028
  exports.generateReleaseNotes = generateReleaseNotes;
474
- exports.readBlueprintData = readBlueprintData;
475
- exports.readPrismDirectory = readPrismDirectory;
3029
+ exports.getDispatchChannel = getDispatchChannel;
3030
+ exports.getPrismTemplate = getPrismTemplate;
3031
+ exports.getSlotValue = getSlotValue;
3032
+ exports.getStyleById = getStyleById;
3033
+ exports.getWidgetTemplate = getWidgetTemplate;
3034
+ exports.initialFormValues = initialFormValues;
3035
+ exports.next7DaysRange = next7DaysRange;
3036
+ exports.nextVersionNumber = nextVersionNumber;
3037
+ exports.orchestrateDispatch = orchestrateDispatch;
3038
+ exports.parseAndValidateSchemaDefinition = parseAndValidateSchemaDefinition;
3039
+ exports.parseBrandKitFromLlmResponse = parseBrandKitFromLlmResponse;
3040
+ exports.parseBrandThemeContent = parseBrandThemeContent;
3041
+ exports.parseStyleFromCss = parseStyleFromCss;
3042
+ exports.parseStyleFromTailwindConfig = parseStyleFromTailwindConfig;
3043
+ exports.parseStyleFromTokensJson = parseStyleFromTokensJson;
3044
+ exports.parseThemeConfigContent = parseThemeConfigContent;
3045
+ exports.previewExport = previewExport;
3046
+ exports.refineAsset = refineAsset;
3047
+ exports.refineLimitState = refineLimitState;
3048
+ exports.renderWidget = renderWidget;
3049
+ exports.resolveAnimatedChoice = resolveAnimatedChoice;
3050
+ exports.resolveAnimationDuration = resolveAnimationDuration;
3051
+ exports.resolveBrandPalette = resolveBrandPalette;
3052
+ exports.runForgeGeneration = runForgeGeneration;
3053
+ exports.scanForSecrets = scanForSecrets;
3054
+ exports.schemaExampleFor = schemaExampleFor;
3055
+ exports.schemaToForm = schemaToForm;
3056
+ exports.templateAnimatedDefault = templateAnimatedDefault;
3057
+ exports.themeEntryToPalette = themeEntryToPalette;
3058
+ exports.tryParseJsonObject = tryParseJsonObject;
3059
+ exports.validateAgainstTemplateSchema = validateAgainstTemplateSchema;
3060
+ exports.validateAssetSlots = validateAssetSlots;
3061
+ exports.validateFormValues = validateFormValues;
3062
+ exports.validateSchemaDefinition = validateSchemaDefinition;