@releasekit/notes 0.2.0-next.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Sam Maister
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @releasekit/notes
2
2
 
3
- Changelog generation with LLM-powered enhancement and flexible templating.
3
+ Changelog generation from conventional commits with LLM-powered enhancement and flexible templating
4
4
 
5
5
  ## Features
6
6
 
@@ -148,7 +148,7 @@ async function parsePackageVersionerStdin() {
148
148
  // src/output/markdown.ts
149
149
  import * as fs2 from "fs";
150
150
  import * as path from "path";
151
- import { info, success } from "@releasekit/core";
151
+ import { debug, info, success } from "@releasekit/core";
152
152
  var TYPE_ORDER = ["added", "changed", "deprecated", "removed", "fixed", "security"];
153
153
  var TYPE_LABELS = {
154
154
  added: "Added",
@@ -247,9 +247,10 @@ ${body}`;
247
247
  function writeMarkdown(outputPath, contexts, config, dryRun) {
248
248
  const content = renderMarkdown(contexts);
249
249
  if (dryRun) {
250
- info("--- Changelog Preview ---");
251
- console.log(content);
252
- info("--- End Preview ---");
250
+ info(`Would write changelog to ${outputPath}`);
251
+ debug("--- Changelog Preview ---");
252
+ debug(content);
253
+ debug("--- End Preview ---");
253
254
  return;
254
255
  }
255
256
  const dir = path.dirname(outputPath);
@@ -275,7 +276,7 @@ function writeMarkdown(outputPath, contexts, config, dryRun) {
275
276
  // src/output/json.ts
276
277
  import * as fs3 from "fs";
277
278
  import * as path2 from "path";
278
- import { info as info2, success as success2 } from "@releasekit/core";
279
+ import { debug as debug2, info as info2, success as success2 } from "@releasekit/core";
279
280
  function renderJson(contexts) {
280
281
  return JSON.stringify(
281
282
  {
@@ -295,9 +296,10 @@ function renderJson(contexts) {
295
296
  function writeJson(outputPath, contexts, dryRun) {
296
297
  const content = renderJson(contexts);
297
298
  if (dryRun) {
298
- info2("--- JSON Output Preview ---");
299
- console.log(content);
300
- info2("--- End Preview ---");
299
+ info2(`Would write JSON output to ${outputPath}`);
300
+ debug2("--- JSON Output Preview ---");
301
+ debug2(content);
302
+ debug2("--- End Preview ---");
301
303
  return;
302
304
  }
303
305
  const dir = path2.dirname(outputPath);
@@ -311,7 +313,7 @@ function writeJson(outputPath, contexts, dryRun) {
311
313
  // src/core/pipeline.ts
312
314
  import * as fs8 from "fs";
313
315
  import * as path6 from "path";
314
- import { debug, info as info4, success as success4, warn as warn3 } from "@releasekit/core";
316
+ import { debug as debug3, info as info4, success as success4, warn as warn3 } from "@releasekit/core";
315
317
 
316
318
  // src/llm/defaults.ts
317
319
  var LLM_DEFAULTS = {
@@ -508,6 +510,83 @@ var OpenAICompatibleProvider = class extends BaseLLMProvider {
508
510
 
509
511
  // src/llm/tasks/categorize.ts
510
512
  import { warn } from "@releasekit/core";
513
+
514
+ // src/llm/prompts.ts
515
+ function resolvePrompt(taskName, defaultPrompt, promptsConfig) {
516
+ if (!promptsConfig) return defaultPrompt;
517
+ const fullTemplate = promptsConfig.templates?.[taskName];
518
+ if (fullTemplate) return fullTemplate;
519
+ const additionalInstructions = promptsConfig.instructions?.[taskName];
520
+ if (additionalInstructions) {
521
+ const insertionPoint = defaultPrompt.lastIndexOf("Output only valid JSON");
522
+ if (insertionPoint !== -1) {
523
+ return `${defaultPrompt.slice(0, insertionPoint)}Additional instructions:
524
+ ${additionalInstructions}
525
+
526
+ ${defaultPrompt.slice(insertionPoint)}`;
527
+ }
528
+ return `${defaultPrompt}
529
+
530
+ Additional instructions:
531
+ ${additionalInstructions}`;
532
+ }
533
+ return defaultPrompt;
534
+ }
535
+
536
+ // src/llm/scopes.ts
537
+ function getAllowedScopesFromCategories(categories) {
538
+ const scopeMap = /* @__PURE__ */ new Map();
539
+ for (const cat of categories) {
540
+ if (cat.scopes && cat.scopes.length > 0) {
541
+ scopeMap.set(cat.name, cat.scopes);
542
+ }
543
+ }
544
+ return scopeMap;
545
+ }
546
+ function resolveAllowedScopes(scopeConfig, categories, packageNames) {
547
+ if (!scopeConfig || scopeConfig.mode === "unrestricted") return null;
548
+ if (scopeConfig.mode === "none") return [];
549
+ if (scopeConfig.mode === "packages") return packageNames ?? [];
550
+ if (scopeConfig.mode === "restricted") {
551
+ const explicit = scopeConfig.rules?.allowed ?? [];
552
+ const all = new Set(explicit);
553
+ if (categories) {
554
+ const fromCategories = getAllowedScopesFromCategories(categories);
555
+ for (const scopes of fromCategories.values()) {
556
+ for (const s of scopes) all.add(s);
557
+ }
558
+ }
559
+ return [...all];
560
+ }
561
+ return null;
562
+ }
563
+ function validateScope(scope, allowedScopes, rules) {
564
+ if (!scope || allowedScopes === null) return scope;
565
+ if (allowedScopes.length === 0) return void 0;
566
+ const caseSensitive = rules?.caseSensitive ?? false;
567
+ const normalise = (s) => caseSensitive ? s : s.toLowerCase();
568
+ const isAllowed = allowedScopes.some((a) => normalise(a) === normalise(scope));
569
+ if (isAllowed) return scope;
570
+ switch (rules?.invalidScopeAction ?? "remove") {
571
+ case "keep":
572
+ return scope;
573
+ case "fallback":
574
+ return rules?.fallbackScope;
575
+ case "remove":
576
+ default:
577
+ return void 0;
578
+ }
579
+ }
580
+ function validateEntryScopes(entries, scopeConfig, categories) {
581
+ const allowedScopes = resolveAllowedScopes(scopeConfig, categories);
582
+ if (allowedScopes === null) return entries;
583
+ return entries.map((entry) => ({
584
+ ...entry,
585
+ scope: validateScope(entry.scope, allowedScopes, scopeConfig?.rules)
586
+ }));
587
+ }
588
+
589
+ // src/llm/tasks/categorize.ts
511
590
  var DEFAULT_CATEGORIZE_PROMPT = `You are categorizing changelog entries for a software release.
512
591
 
513
592
  Given the following entries, group them into meaningful categories (e.g., "Core", "UI", "API", "Performance", "Bug Fixes", "Documentation").
@@ -519,20 +598,21 @@ Entries:
519
598
 
520
599
  Output only valid JSON, nothing else:`;
521
600
  function buildCustomCategorizePrompt(categories) {
522
- const categoryList = categories.map((c) => `- "${c.name}": ${c.description}`).join("\n");
523
- const developerCategory = categories.find((c) => c.name === "Developer");
601
+ const categoryList = categories.map((c) => {
602
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
603
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
604
+ }).join("\n");
605
+ const scopeMap = getAllowedScopesFromCategories(categories);
524
606
  let scopeInstructions = "";
525
- if (developerCategory) {
526
- const scopeMatch = developerCategory.description.match(/from:\s*([^.]+)/);
527
- if (scopeMatch?.[1]) {
528
- const scopes = scopeMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
529
- if (scopes.length > 0) {
530
- scopeInstructions = `
531
-
532
- For the "Developer" category, you MUST assign a scope from this exact list: ${scopes.join(", ")}.
533
- `;
534
- }
607
+ if (scopeMap.size > 0) {
608
+ const entries = [];
609
+ for (const [catName, scopes] of scopeMap) {
610
+ entries.push(`For "${catName}", assign a scope from: ${scopes.join(", ")}.`);
535
611
  }
612
+ scopeInstructions = `
613
+
614
+ ${entries.join("\n")}
615
+ Only use scopes from these predefined lists. If an entry does not fit any scope, set scope to null.`;
536
616
  }
537
617
  return `You are categorizing changelog entries for a software release.
538
618
 
@@ -540,9 +620,10 @@ Given the following entries, group them into the specified categories. Only use
540
620
 
541
621
  Categories:
542
622
  ${categoryList}${scopeInstructions}
623
+
543
624
  Output a JSON object with two fields:
544
625
  - "categories": an object where keys are category names and values are arrays of entry indices (0-based)
545
- - "scopes": an object where keys are entry indices (as strings) and values are scope labels
626
+ - "scopes": an object where keys are entry indices (as strings) and values are scope labels. Only include entries that have a valid scope from the predefined list.
546
627
 
547
628
  Entries:
548
629
  {{entries}}
@@ -553,9 +634,11 @@ async function categorizeEntries(provider, entries, context) {
553
634
  if (entries.length === 0) {
554
635
  return [];
555
636
  }
556
- const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
637
+ const entriesCopy = entries.map((e) => ({ ...e, scope: void 0 }));
638
+ const entriesText = entriesCopy.map((e, i) => `${i}. [${e.type}]: ${e.description}`).join("\n");
557
639
  const hasCustomCategories = context.categories && context.categories.length > 0;
558
- const promptTemplate = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
640
+ const defaultPrompt = hasCustomCategories ? buildCustomCategorizePrompt(context.categories) : DEFAULT_CATEGORIZE_PROMPT;
641
+ const promptTemplate = resolvePrompt("categorize", defaultPrompt, context.prompts);
559
642
  const prompt = promptTemplate.replace("{{entries}}", entriesText);
560
643
  try {
561
644
  const response = await provider.complete(prompt);
@@ -567,13 +650,14 @@ async function categorizeEntries(provider, entries, context) {
567
650
  const scopeMap = parsed.scopes || {};
568
651
  for (const [indexStr, scope] of Object.entries(scopeMap)) {
569
652
  const idx = Number.parseInt(indexStr, 10);
570
- if (entries[idx] && scope) {
571
- entries[idx] = { ...entries[idx], scope };
653
+ if (entriesCopy[idx] && scope && scope.trim()) {
654
+ entriesCopy[idx] = { ...entriesCopy[idx], scope: scope.trim() };
572
655
  }
573
656
  }
657
+ const validatedEntries = validateEntryScopes(entriesCopy, context.scopes, context.categories);
574
658
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
575
659
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
576
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
660
+ const categoryEntries = indices.map((i) => validatedEntries[i]).filter((e) => e !== void 0);
577
661
  if (categoryEntries.length > 0) {
578
662
  result.push({ category, entries: categoryEntries });
579
663
  }
@@ -582,7 +666,7 @@ async function categorizeEntries(provider, entries, context) {
582
666
  const categoryMap = parsed;
583
667
  for (const [category, rawIndices] of Object.entries(categoryMap)) {
584
668
  const indices = Array.isArray(rawIndices) ? rawIndices : [];
585
- const categoryEntries = indices.map((i) => entries[i]).filter((e) => e !== void 0);
669
+ const categoryEntries = indices.map((i) => entriesCopy[i]).filter((e) => e !== void 0);
586
670
  if (categoryEntries.length > 0) {
587
671
  result.push({ category, entries: categoryEntries });
588
672
  }
@@ -593,12 +677,12 @@ async function categorizeEntries(provider, entries, context) {
593
677
  warn(
594
678
  `LLM categorization failed, falling back to General: ${error instanceof Error ? error.message : String(error)}`
595
679
  );
596
- return [{ category: "General", entries }];
680
+ return [{ category: "General", entries: entriesCopy }];
597
681
  }
598
682
  }
599
683
 
600
684
  // src/llm/tasks/enhance.ts
601
- var ENHANCE_PROMPT = `You are improving changelog entries for a software project.
685
+ var DEFAULT_ENHANCE_PROMPT = `You are improving changelog entries for a software project.
602
686
  Given a technical commit message, rewrite it as a clear, user-friendly changelog entry.
603
687
 
604
688
  Rules:
@@ -614,9 +698,10 @@ Type: {{type}}
614
698
  Description: {{description}}
615
699
 
616
700
  Rewritten description (only output the new description, nothing else):`;
617
- async function enhanceEntry(provider, entry, _context) {
618
- const styleText = _context.style ? `- ${_context.style}` : '- Use present tense ("Add feature" not "Added feature")';
619
- const prompt = ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
701
+ async function enhanceEntry(provider, entry, context) {
702
+ const styleText = context.style ? `- ${context.style}` : '- Use present tense ("Add feature" not "Added feature")';
703
+ const defaultPrompt = DEFAULT_ENHANCE_PROMPT.replace("{{style}}", styleText).replace("{{type}}", entry.type).replace("{{#if scope}}Scope: {{scope}}{{/if}}", entry.scope ? `Scope: ${entry.scope}` : "").replace("{{description}}", entry.description);
704
+ const prompt = resolvePrompt("enhance", defaultPrompt, context.prompts);
620
705
  const response = await provider.complete(prompt);
621
706
  return response.trim();
622
707
  }
@@ -672,7 +757,23 @@ function buildPrompt(entries, categories, style) {
672
757
  const entriesText = entries.map((e, i) => `${i}. [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
673
758
  const styleText = style || 'Use present tense ("Add feature" not "Added feature"). Be concise.';
674
759
  const categorySection = categories ? `Categories (use ONLY these):
675
- ${categories.map((c) => `- "${c.name}": ${c.description}`).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
760
+ ${categories.map((c) => {
761
+ const scopeInfo = c.scopes?.length ? ` Allowed scopes: ${c.scopes.join(", ")}.` : "";
762
+ return `- "${c.name}": ${c.description}${scopeInfo}`;
763
+ }).join("\n")}` : `Categories: Group into meaningful categories (e.g., "New", "Fixed", "Changed", "Removed").`;
764
+ let scopeInstruction = "";
765
+ if (categories) {
766
+ const scopeMap = getAllowedScopesFromCategories(categories);
767
+ if (scopeMap.size > 0) {
768
+ const parts = [];
769
+ for (const [catName, scopes] of scopeMap) {
770
+ parts.push(`For "${catName}" entries, assign a scope from: ${scopes.join(", ")}.`);
771
+ }
772
+ scopeInstruction = `
773
+ ${parts.join("\n")}
774
+ Only use scopes from these predefined lists. Set scope to null if no scope applies.`;
775
+ }
776
+ }
676
777
  return `You are generating release notes for a software project. Given the following changelog entries, do two things:
677
778
 
678
779
  1. **Rewrite** each entry as a clear, user-friendly description
@@ -683,9 +784,7 @@ Style guidelines:
683
784
  - Be concise (1 short sentence per entry)
684
785
  - Focus on what changed, not implementation details
685
786
 
686
- ${categorySection}
687
-
688
- ${categories ? 'For entries in categories involving internal/developer changes, set a "scope" field with a short subcategory label (e.g., "CI", "Dependencies", "Testing").' : ""}
787
+ ${categorySection}${scopeInstruction}
689
788
 
690
789
  Entries:
691
790
  ${entriesText}
@@ -702,7 +801,8 @@ async function enhanceAndCategorize(provider, entries, context) {
702
801
  const retryOpts = LLM_DEFAULTS.retry;
703
802
  try {
704
803
  return await withRetry(async () => {
705
- const prompt = buildPrompt(entries, context.categories, context.style);
804
+ const defaultPrompt = buildPrompt(entries, context.categories, context.style);
805
+ const prompt = resolvePrompt("enhanceAndCategorize", defaultPrompt, context.prompts);
706
806
  const response = await provider.complete(prompt);
707
807
  const cleaned = response.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "").trim();
708
808
  const parsed = JSON.parse(cleaned);
@@ -718,22 +818,23 @@ async function enhanceAndCategorize(provider, entries, context) {
718
818
  scope: result.scope || original.scope
719
819
  };
720
820
  });
821
+ const validatedEntries = validateEntryScopes(enhancedEntries, context.scopes, context.categories);
721
822
  const categoryMap = /* @__PURE__ */ new Map();
722
823
  for (let i = 0; i < parsed.entries.length; i++) {
723
824
  const result = parsed.entries[i];
724
825
  const category = result?.category || "General";
725
- const entry = enhancedEntries[i];
826
+ const entry = validatedEntries[i];
726
827
  if (!entry) continue;
727
828
  if (!categoryMap.has(category)) {
728
829
  categoryMap.set(category, []);
729
830
  }
730
- categoryMap.get(category).push(entry);
831
+ categoryMap.get(category)?.push(entry);
731
832
  }
732
833
  const categories = [];
733
834
  for (const [category, catEntries] of categoryMap) {
734
835
  categories.push({ category, entries: catEntries });
735
836
  }
736
- return { enhancedEntries, categories };
837
+ return { enhancedEntries: validatedEntries, categories };
737
838
  }, retryOpts);
738
839
  } catch (error) {
739
840
  warn2(
@@ -747,7 +848,7 @@ async function enhanceAndCategorize(provider, entries, context) {
747
848
  }
748
849
 
749
850
  // src/llm/tasks/release-notes.ts
750
- var RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
851
+ var DEFAULT_RELEASE_NOTES_PROMPT = `You are writing release notes for a software project.
751
852
 
752
853
  Create engaging, user-friendly release notes for the following changes.
753
854
 
@@ -780,16 +881,17 @@ No notable changes in this release.`;
780
881
  if (e.breaking) line += " **BREAKING**";
781
882
  return line;
782
883
  }).join("\n");
783
- const prompt = RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
884
+ const defaultPrompt = DEFAULT_RELEASE_NOTES_PROMPT.replace("{{version}}", context.version ?? "v1.0.0").replace(
784
885
  "{{#if previousVersion}}Previous version: {{previousVersion}}{{/if}}",
785
886
  context.previousVersion ? `Previous version: ${context.previousVersion}` : ""
786
887
  ).replace("{{date}}", context.date ?? (/* @__PURE__ */ new Date()).toISOString().split("T")[0] ?? "").replace("{{entries}}", entriesText);
888
+ const prompt = resolvePrompt("releaseNotes", defaultPrompt, context.prompts);
787
889
  const response = await provider.complete(prompt);
788
890
  return response.trim();
789
891
  }
790
892
 
791
893
  // src/llm/tasks/summarize.ts
792
- var SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
894
+ var DEFAULT_SUMMARIZE_PROMPT = `You are creating a summary of changes for a software release.
793
895
 
794
896
  Given the following changelog entries, create a brief summary (2-3 sentences) that captures the main themes of this release.
795
897
 
@@ -797,12 +899,13 @@ Entries:
797
899
  {{entries}}
798
900
 
799
901
  Summary (only output the summary, nothing else):`;
800
- async function summarizeEntries(provider, entries, _context) {
902
+ async function summarizeEntries(provider, entries, context) {
801
903
  if (entries.length === 0) {
802
904
  return "";
803
905
  }
804
906
  const entriesText = entries.map((e) => `- [${e.type}]${e.scope ? ` (${e.scope})` : ""}: ${e.description}`).join("\n");
805
- const prompt = SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
907
+ const defaultPrompt = DEFAULT_SUMMARIZE_PROMPT.replace("{{entries}}", entriesText);
908
+ const prompt = resolvePrompt("summarize", defaultPrompt, context.prompts);
806
909
  const response = await provider.complete(prompt);
807
910
  return response.trim();
808
911
  }
@@ -1214,17 +1317,27 @@ function renderTemplate(templatePath, context, engine) {
1214
1317
  }
1215
1318
 
1216
1319
  // src/core/pipeline.ts
1217
- function generateCompareUrl(repoUrl, from, to) {
1320
+ function generateCompareUrl(repoUrl, from, to, packageName) {
1321
+ const isPackageSpecific = from.includes("@") && packageName && from.includes(packageName);
1322
+ let fromVersion;
1323
+ let toVersion;
1324
+ if (isPackageSpecific) {
1325
+ fromVersion = from;
1326
+ toVersion = `${packageName}@${to.startsWith("v") ? "" : "v"}${to}`;
1327
+ } else {
1328
+ fromVersion = from.replace(/^v/, "");
1329
+ toVersion = to.replace(/^v/, "");
1330
+ }
1218
1331
  if (/gitlab\.com/i.test(repoUrl)) {
1219
- return `${repoUrl}/-/compare/${from}...${to}`;
1332
+ return `${repoUrl}/-/compare/${fromVersion}...${toVersion}`;
1220
1333
  }
1221
1334
  if (/bitbucket\.org/i.test(repoUrl)) {
1222
- return `${repoUrl}/branches/compare/${from}..${to}`;
1335
+ return `${repoUrl}/branches/compare/${fromVersion}..${toVersion}`;
1223
1336
  }
1224
- return `${repoUrl}/compare/${from}...${to}`;
1337
+ return `${repoUrl}/compare/${fromVersion}...${toVersion}`;
1225
1338
  }
1226
1339
  function createTemplateContext(pkg) {
1227
- const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version) : void 0;
1340
+ const compareUrl = pkg.repoUrl && pkg.previousVersion ? generateCompareUrl(pkg.repoUrl, pkg.previousVersion, pkg.version, pkg.packageName) : void 0;
1228
1341
  return {
1229
1342
  packageName: pkg.packageName,
1230
1343
  version: pkg.version,
@@ -1262,7 +1375,9 @@ async function processWithLLM(context, config) {
1262
1375
  previousVersion: context.previousVersion ?? void 0,
1263
1376
  date: context.date,
1264
1377
  categories: config.llm.categories,
1265
- style: config.llm.style
1378
+ style: config.llm.style,
1379
+ scopes: config.llm.scopes,
1380
+ prompts: config.llm.prompts
1266
1381
  };
1267
1382
  const enhanced = {
1268
1383
  entries: context.entries
@@ -1310,7 +1425,7 @@ async function processWithLLM(context, config) {
1310
1425
  enhanced.summary = await summarizeEntries(provider, enhanced.entries, llmContext);
1311
1426
  if (enhanced.summary) {
1312
1427
  info4("Summary generated successfully");
1313
- debug(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1428
+ debug3(`Summary: ${enhanced.summary.substring(0, 100)}...`);
1314
1429
  } else {
1315
1430
  warn3("Summary generation returned empty result");
1316
1431
  }
@@ -1326,7 +1441,6 @@ async function processWithLLM(context, config) {
1326
1441
  }
1327
1442
  return {
1328
1443
  ...context,
1329
- entries: enhanced.entries,
1330
1444
  enhanced
1331
1445
  };
1332
1446
  } catch (error) {
@@ -1359,9 +1473,10 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1359
1473
  );
1360
1474
  const result = renderTemplate(templatePath, documentContext, config.templates?.engine);
1361
1475
  if (dryRun) {
1362
- info4("--- Changelog Preview ---");
1363
- console.log(result.content);
1364
- info4("--- End Preview ---");
1476
+ info4(`Would write templated output to ${outputPath}`);
1477
+ debug3("--- Changelog Preview ---");
1478
+ debug3(result.content);
1479
+ debug3("--- End Preview ---");
1365
1480
  return;
1366
1481
  }
1367
1482
  if (outputPath === "-") {
@@ -1376,7 +1491,7 @@ async function generateWithTemplate(contexts, config, outputPath, dryRun) {
1376
1491
  success4(`Changelog written to ${outputPath} (using ${result.engine} template)`);
1377
1492
  }
1378
1493
  async function runPipeline(input, config, dryRun) {
1379
- debug(`Processing ${input.packages.length} package(s)`);
1494
+ debug3(`Processing ${input.packages.length} package(s)`);
1380
1495
  let contexts = input.packages.map(createTemplateContext);
1381
1496
  if (config.llm && !process.env.CHANGELOG_NO_LLM) {
1382
1497
  info4("Processing with LLM enhancement");
@@ -1387,8 +1502,10 @@ async function runPipeline(input, config, dryRun) {
1387
1502
  switch (output.format) {
1388
1503
  case "markdown": {
1389
1504
  const file = output.file ?? "CHANGELOG.md";
1390
- if (config.templates?.path || output.options?.template) {
1391
- await generateWithTemplate(contexts, config, file, dryRun);
1505
+ const effectiveTemplateConfig = output.templates ?? config.templates;
1506
+ if (effectiveTemplateConfig?.path || output.options?.template) {
1507
+ const configWithTemplate = { ...config, templates: effectiveTemplateConfig };
1508
+ await generateWithTemplate(contexts, configWithTemplate, file, dryRun);
1392
1509
  } else {
1393
1510
  writeMarkdown(file, contexts, config, dryRun);
1394
1511
  }
@@ -1438,7 +1555,7 @@ async function processInput(inputJson, config, dryRun) {
1438
1555
  // src/monorepo/aggregator.ts
1439
1556
  import * as fs9 from "fs";
1440
1557
  import * as path7 from "path";
1441
- import { info as info5, success as success5 } from "@releasekit/core";
1558
+ import { debug as debug4, info as info5, success as success5 } from "@releasekit/core";
1442
1559
 
1443
1560
  // src/monorepo/splitter.ts
1444
1561
  function splitByPackage(contexts) {
@@ -1452,8 +1569,8 @@ function splitByPackage(contexts) {
1452
1569
  // src/monorepo/aggregator.ts
1453
1570
  function writeFile(outputPath, content, dryRun) {
1454
1571
  if (dryRun) {
1455
- info5(`[DRY RUN] Would write to ${outputPath}`);
1456
- console.log(content);
1572
+ info5(`Would write to ${outputPath}`);
1573
+ debug4(content);
1457
1574
  return;
1458
1575
  }
1459
1576
  const dir = path7.dirname(outputPath);