Update AGENTS.md, RELEASING.md, contributing.md with the structured changeset format guide. Add .changeset/README.md template. Add changeset for this change. Update distill-release-notes.mjs with final version.
113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
/*
|
|
* FNXC:Changelog 2026-06-24-15:30:
|
|
* Release-notes distillation module. Transforms parsed changeset entries
|
|
* into grouped, end-user-facing release notes. The deterministic fallback
|
|
* builds a category-grouped bullet list directly from the structured
|
|
* `summary` fields — no model call. When a model is available, the prompt
|
|
* and system prompt defined here can be used to produce curated, polished
|
|
* notes via `createFnAgent`.
|
|
*
|
|
* Audience is Fusion operators: behavior, fixes, what changed — minimal
|
|
* internals. The `dev` field is preserved in per-package CHANGELOGs but
|
|
* excluded from distilled release notes by default.
|
|
*/
|
|
|
|
import { CATEGORIES, CATEGORY_HEADINGS } from "./changeset-schema.mjs";
|
|
|
|
/**
|
|
* System prompt for AI distillation via `createFnAgent`.
|
|
* Instructs the model to produce grouped markdown release notes for a
|
|
* Fusion operator audience, using only the `summary` fields as input.
|
|
*/
|
|
export const DISTILLATION_SYSTEM_PROMPT = [
|
|
"You are a release-notes writer for Fusion, a model-agnostic AI agent orchestration product.",
|
|
"Your audience is Fusion operators — developers using the product, not its internals.",
|
|
"Produce clean, grouped markdown release notes from the provided changeset entries.",
|
|
"Group under these headings (omit empty sections):",
|
|
" ### New (features)",
|
|
" ### Fixed (bug fixes)",
|
|
" ### Breaking (breaking changes)",
|
|
" ### Security (security fixes)",
|
|
" ### Performance (performance improvements)",
|
|
" ### Internal (internal-only changes)",
|
|
"Rules:",
|
|
"- Use the `summary` text verbatim or lightly edited for clarity and grouping.",
|
|
"- Do NOT include internal class names, file paths, or implementation detail.",
|
|
"- Do NOT include the `dev` field content unless it is user-relevant migration guidance.",
|
|
"- Write one bullet per entry, prefixed with `- `.",
|
|
"- Omit empty sections entirely.",
|
|
"- Do NOT add a title or version heading — only the grouped sections.",
|
|
"- Respond with markdown only, no preamble or explanation.",
|
|
].join("\n");
|
|
|
|
/**
|
|
* Build the user-facing prompt for AI distillation.
|
|
* Lists each entry as `[N] category: X / summary: Y / dev: Z`.
|
|
*
|
|
* @param {Array<{summary: string, category: string, dev?: string, legacy?: boolean}>} entries
|
|
* @returns {string}
|
|
*/
|
|
export function buildDistillationPrompt(entries) {
|
|
const lines = ["Produce release notes from these changeset entries:\n"];
|
|
entries.forEach((entry, i) => {
|
|
const num = i + 1;
|
|
lines.push(`[${num}]`);
|
|
lines.push(` category: ${entry.category}`);
|
|
lines.push(` summary: ${entry.summary}`);
|
|
if (entry.dev) {
|
|
lines.push(` dev: ${entry.dev}`);
|
|
}
|
|
lines.push("");
|
|
});
|
|
return lines.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Deterministic fallback: build category-grouped release notes directly
|
|
* from the structured `summary` fields — no model call.
|
|
*
|
|
* Used when:
|
|
* - The model call fails, times out, or returns unparseable output
|
|
* - No model is configured (CI without model secret)
|
|
* - As a pre-model preview in dry-runs
|
|
*
|
|
* @param {Array<{summary: string, category: string, legacy?: boolean}>} entries
|
|
* @param {string} version - Target version string (e.g. "0.47.0")
|
|
* @returns {{notes: string, source: "deterministic"}}
|
|
*/
|
|
export function distillDeterministic(entries, version) {
|
|
if (!entries || entries.length === 0) {
|
|
return {
|
|
notes: `No changes in v${version}.`,
|
|
source: "deterministic",
|
|
};
|
|
}
|
|
|
|
// Group entries by category, preserving entry order within each group.
|
|
const groups = new Map();
|
|
for (const cat of CATEGORIES) {
|
|
groups.set(cat, []);
|
|
}
|
|
|
|
for (const entry of entries) {
|
|
const cat = groups.has(entry.category) ? entry.category : "internal";
|
|
groups.get(cat).push(entry.summary);
|
|
}
|
|
|
|
// Build sections in display order, omitting empty categories.
|
|
const sections = [];
|
|
for (const cat of CATEGORIES) {
|
|
const summaries = groups.get(cat);
|
|
if (summaries.length === 0) continue;
|
|
|
|
const heading = CATEGORY_HEADINGS[cat];
|
|
const bullets = summaries.map((s) => `- ${s}`).join("\n");
|
|
sections.push(`### ${heading}\n\n${bullets}`);
|
|
}
|
|
|
|
return {
|
|
notes: sections.join("\n\n"),
|
|
source: "deterministic",
|
|
};
|
|
}
|