/* Facilitates changelog management for applications, including generation, rebuilding, and backfilling of changelogs. Do not handle raw commit parsing or direct Git interactions outside of changelog context. Do not manage application configuration or transport orchestration. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt */ import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { buildChangelogCtx, readFlag } from "./changelog/context.ts"; import type { ChangelogCtx } from "./changelog/context.ts"; import type { SystemState } from "./changelog/types.ts"; import { isTodayReleaseDay, getPeriodWindow, formatDateISO } from "./changelog/utils/date.ts"; import type { Schedule } from "./changelog/utils/date.ts"; import { collectCommits, getLatestCommitHash, getAppVersion } from "./changelog/utils/git.ts"; import { sanitizeCommit } from "./changelog/utils/sanitize.ts"; import { buildCommitLinksMap } from "./changelog/utils/link-extractor.ts"; import { classifyBatch } from "./changelog/agents/classifier-agent.ts"; import { groupCommits } from "./changelog/agents/grouper-agent.ts"; import { writeChangelog } from "./changelog/agents/writer-agent.ts"; import { calculateNextVersion } from "./changelog/core/version-bumper.ts"; import { writeVersionedFile, rebuildIndex } from "./changelog/core/index-rebuilder.ts"; import { failResult } from "@warpgogol/werkstatt-shared/checks/result-helpers"; // START_BLOCK_STATE async function loadState(ctx: ChangelogCtx): Promise { try { return JSON.parse(await ctx.io.readFile(ctx.statePath)) as SystemState; } catch { return { lastProcessedCommitHash: "", lastVersion: "", processedWindows: [] }; } } async function saveState(state: SystemState, ctx: ChangelogCtx): Promise { await ctx.io.mkdir(ctx.statePath.replace(/[/\\][^/\\]+$/, "")); await ctx.io.writeFile(ctx.statePath, JSON.stringify(state, null, 2)); } // END_BLOCK_STATE // START_BLOCK_GENERATE /** * Full changelog generation pipeline scoped to the target app. * [CL-MAIN][runChangelogGenerate][STARTED] app={ctx.appName} */ export async function runChangelogGenerate( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const ctx = buildChangelogCtx(input, context); context.logger.info( `[CL-MAIN][runChangelogGenerate][STARTED] app=${ctx.appName} dryRun=${ctx.dryRun}`, ); const schedule: Schedule = ctx.releaseMode === "weekly" ? { mode: "weekly", weekday: ctx.releaseDay as 0 | 1 | 2 | 3 | 4 | 5 | 6 } : { mode: "monthly", dayOfMonth: ctx.releaseDay }; if (!ctx.force && !isTodayReleaseDay(schedule, ctx.timezone)) { context.logger.info( `[CL-MAIN][runChangelogGenerate][SKIPPED] Not a release day. Use --force to override.`, ); return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: skipped — not a release day`, }; } const { from, to } = getPeriodWindow(schedule, ctx.timezone); const fromISO = from.toISOString(); const toISO = to.toISOString(); const state = await loadState(ctx); if (!ctx.force && state.processedWindows.some((w) => w.from === fromISO && w.to === toISO)) { context.logger.info("[CL-MAIN][runChangelogGenerate][SKIPPED] Window already processed."); return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: skipped — window already processed`, }; } const rawCommits = await collectCommits(fromISO, toISO, ctx); context.logger.info(`[CL-MAIN][runChangelogGenerate][COLLECTED] count=${rawCommits.length}`); if (rawCommits.length === 0) { return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: no commits in window`, }; } const sanitized = rawCommits.map(sanitizeCommit); const classified = await classifyBatch(sanitized, ctx); const nonSkip = classified.filter((c) => c.type !== "skip"); if (nonSkip.length === 0) { return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: all commits are noise`, }; } const grouped = await groupCommits(classified, ctx); const currentVersion = await getAppVersion(ctx.appDir, ctx.io); const bump = calculateNextVersion(currentVersion, classified, ctx.confidenceThreshold); context.logger.info( `[CL-MAIN][runChangelogGenerate][BUMPED] ${currentVersion} → ${bump.version}`, ); if (bump.requiresReview) { context.logger.warn( `[CL-MAIN][runChangelogGenerate][REVIEW_REQUIRED] Low-confidence breaking changes detected.`, ); } const commitLinks = buildCommitLinksMap(rawCommits, ctx.repoUrl); const content = await writeChangelog(grouped, bump.version, ctx, commitLinks); if (ctx.dryRun) { context.logger.info("--- DRY RUN OUTPUT ---\n" + content); return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: dry-run complete — version would be ${bump.version}`, }; } const date = formatDateISO(new Date()); const filename = await writeVersionedFile(date, bump.version, content, ctx); await rebuildIndex(date, bump.version, filename, ctx); await bumpAppVersion(bump.version, ctx); const headHash = await getLatestCommitHash(ctx.workspaceRoot); state.lastProcessedCommitHash = headHash; state.lastVersion = bump.version; state.processedWindows.push({ from: fromISO, to: toISO, version: bump.version, checksum: headHash, }); await saveState(state, ctx); context.logger.info( `[CL-MAIN][runChangelogGenerate][DONE] version=${bump.version} file=${filename}`, ); return { data: { version: bump.version, filename }, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: changelog generated → ${bump.version} (${filename})`, }; } // END_BLOCK_GENERATE // START_BLOCK_REBUILD export async function runChangelogRebuildIndex( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const ctx = buildChangelogCtx(input, context); const state = await loadState(ctx); const date = formatDateISO(new Date()); const version = state.lastVersion || (await getAppVersion(ctx.appDir, ctx.io)); const filename = `changelog-${date}-v${version}.md`; await rebuildIndex(date, version, filename, ctx); return { data: { indexFile: ctx.indexFile }, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: index rebuilt at ${ctx.indexFile}`, }; } // END_BLOCK_REBUILD // START_BLOCK_BACKFILL export async function runChangelogBackfill( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const ctx = buildChangelogCtx(input, context); const start = readFlag(input, "start"); const end = readFlag(input, "end"); if (!start || !end) { return { ...failResult("changelog.command", [ "backfill requires --start and --end flags (YYYY-MM-DD)", ]), data: null, }; } const rawCommits = await collectCommits( new Date(start).toISOString(), new Date(end).toISOString(), ctx, ); const classified = await classifyBatch(rawCommits.map(sanitizeCommit), ctx); const grouped = await groupCommits(classified, ctx); const currentVersion = await getAppVersion(ctx.appDir, ctx.io); const bump = calculateNextVersion(currentVersion, classified, ctx.confidenceThreshold); const commitLinks = buildCommitLinksMap(rawCommits, ctx.repoUrl); const content = await writeChangelog(grouped, bump.version, ctx, commitLinks); if (ctx.dryRun) { context.logger.info(content); return { data: null, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: backfill dry-run — version would be ${bump.version}`, }; } const filename = await writeVersionedFile(start, bump.version, content, ctx); await rebuildIndex(start, bump.version, filename, ctx); return { data: { version: bump.version }, exitCode: 0, summary: `[changelog.command] ${ctx.appName}: backfill complete → ${bump.version}`, }; } // END_BLOCK_BACKFILL // START_BLOCK_HELPERS async function bumpAppVersion(newVersion: string, ctx: ChangelogCtx): Promise { const pkgPath = `${ctx.appDir}/package.json`; try { const raw = await ctx.io.readFile(pkgPath); const pkg = JSON.parse(raw) as Record; pkg["version"] = newVersion; await ctx.io.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n"); } catch (err) { console.warn("[CL-MAIN][bumpAppVersion][FAILED]", err); } } // END_BLOCK_HELPERS