From e8524bedc4a3839975f36052e3b5babf9283f8b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 19:48:00 +0100 Subject: [PATCH 1/2] fix(notes): drop an entry's own refs from LLM-rewritten descriptions The model sometimes copies an entry's reference into its rewrite, and templates then append it again: "(#608) (#608)". The enhance tasks now remove the entry's own refs from the description they return. Closes #719 Co-Authored-By: Claude Opus 5.5 --- packages/core/src/changelogRefs.ts | 11 +++++++++++ packages/core/src/index.ts | 1 + packages/core/test/unit/changelogRefs.spec.ts | 10 ++++++++++ .../notes/src/llm/tasks/enhance-and-categorize.ts | 4 ++-- packages/notes/src/llm/tasks/enhance.ts | 7 +++++-- 5 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/core/src/changelogRefs.ts b/packages/core/src/changelogRefs.ts index d1a660b8..a316a73b 100644 --- a/packages/core/src/changelogRefs.ts +++ b/packages/core/src/changelogRefs.ts @@ -126,3 +126,14 @@ export function neutralizeDescriptionRefs( // `( ?)` only eats the space before a ref, so one removed from the very start leaves the following space behind. return out.trim(); } + +/** Removes each ref in `refs` from `text`, leaving any other ref as written. */ +export function removeDescriptionRefs(text: string, refs: Array): string { + const remove = new Set(refs.filter((r): r is string => !!r).map((r) => Number(r.replace(/^#/, '')))); + return text + .replace(CODE_SPAN_OR_DESC_REF, (match, _lead: string | undefined, parenNum?: string, bareNum?: string) => { + const num = parenNum ?? bareNum; + return num !== undefined && remove.has(Number(num)) ? '' : match; + }) + .trim(); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fc3535aa..9492c93e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,6 +10,7 @@ export { escapeChangelogMentions, neutralizeDescriptionRefs, parseGitHubOwnerRepo, + removeDescriptionRefs, renderIssueRefs, } from './changelogRefs.js'; export { readPackageVersion, writeEnvelope, writeErrorEnvelope } from './cli.js'; diff --git a/packages/core/test/unit/changelogRefs.spec.ts b/packages/core/test/unit/changelogRefs.spec.ts index 5e422b1e..f6094d12 100644 --- a/packages/core/test/unit/changelogRefs.spec.ts +++ b/packages/core/test/unit/changelogRefs.spec.ts @@ -3,6 +3,7 @@ import { escapeChangelogMentions, neutralizeDescriptionRefs, parseGitHubOwnerRepo, + removeDescriptionRefs, renderIssueRefs, } from '../../src/changelogRefs.js'; @@ -200,3 +201,12 @@ describe('neutralizeDescriptionRefs', () => { expect(neutralizeDescriptionRefs('#999 see here', [], 'strip', repo)).toBe('see here'); }); }); + +describe('removeDescriptionRefs', () => { + it('should remove the given refs and leave other refs and code spans as written', () => { + expect(removeDescriptionRefs('Fixed the parser (#720), see #12 (#720)', ['#720'])).toBe( + 'Fixed the parser, see #12', + ); + expect(removeDescriptionRefs('Kept `#720` in code (#720)', ['#720'])).toBe('Kept `#720` in code'); + }); +}); diff --git a/packages/notes/src/llm/tasks/enhance-and-categorize.ts b/packages/notes/src/llm/tasks/enhance-and-categorize.ts index 615157bd..1b02bedb 100644 --- a/packages/notes/src/llm/tasks/enhance-and-categorize.ts +++ b/packages/notes/src/llm/tasks/enhance-and-categorize.ts @@ -1,4 +1,4 @@ -import { warn } from '@releasekit/core'; +import { removeDescriptionRefs, warn } from '@releasekit/core'; import type { ChangelogEntry, LLMCategory } from '../../core/types.js'; import { LLMError } from '../../errors/index.js'; import { LLM_DEFAULTS } from '../defaults.js'; @@ -105,7 +105,7 @@ export function createEnhanceAndCategorizeValidator( } return { ...original, - description: llmEntry.description, + description: removeDescriptionRefs(llmEntry.description, [...(original.issueIds ?? []), original.prNumber]), scope: llmEntry.scope ?? undefined, breaking: original.breaking || llmEntry.breaking === true, leadIn: llmEntry.leadIn ?? undefined, diff --git a/packages/notes/src/llm/tasks/enhance.ts b/packages/notes/src/llm/tasks/enhance.ts index 0de48869..ad1e15b8 100644 --- a/packages/notes/src/llm/tasks/enhance.ts +++ b/packages/notes/src/llm/tasks/enhance.ts @@ -1,4 +1,4 @@ -import { warn } from '@releasekit/core'; +import { removeDescriptionRefs, warn } from '@releasekit/core'; import type { ChangelogEntry } from '../../core/types.js'; import { LLM_DEFAULTS } from '../defaults.js'; import type { EnhanceContext, LLMProvider } from '../index.js'; @@ -58,7 +58,10 @@ export async function enhanceEntries( batch.map(async (entry) => { try { const newDescription = await enhanceEntry(provider, entry, context); - return { ...entry, description: newDescription }; + return { + ...entry, + description: removeDescriptionRefs(newDescription, [...(entry.issueIds ?? []), entry.prNumber]), + }; } catch { // Keep the raw entry so one failure doesn't sink the batch. failures++; From 16ab70ab045bf6113316fc06ba1c7c27460b3953 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 23 Oct 2026 20:07:09 +0100 Subject: [PATCH 2/2] test(notes): assert enhance-and-categorize strips duplicate refs Co-authored-by: Cursor --- packages/notes/test/unit/llm/tasks.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/notes/test/unit/llm/tasks.spec.ts b/packages/notes/test/unit/llm/tasks.spec.ts index af0642c6..cae5e257 100644 --- a/packages/notes/test/unit/llm/tasks.spec.ts +++ b/packages/notes/test/unit/llm/tasks.spec.ts @@ -220,6 +220,20 @@ describe('createEnhanceAndCategorizeValidator()', () => { expect(result.error).toBe('Unknown categories: Mystery. Valid categories: New, Fixed, Changed'); }); + it('should strip refs the renderer will append from an enhanced description', () => { + const validate = createEnhanceAndCategorizeValidator( + [{ type: 'changed', description: 'Rename preset', prNumber: '608' }], + ctx, + ); + const result = validate(structured({ entries: [fullEntry('Renamed the preset (#608)', 'Changed')] })); + + expect(result.valid).toBe(true); + if (!result.valid) { + return; + } + expect(result.value.enhancedEntries[0]?.description).toBe('Renamed the preset'); + }); + it.each([ ['keep a breaking change breaking when the model says it is not', true, false], ['mark an entry breaking when the model says it is', undefined, true],