Files
paseo/packages/server/src/utils/build-metadata-prompt.ts
T
Mohamed Boudra fc4c2aa367 Decouple metadata prompt contract from overridable style
Metadata prompts (workspace title + branch, commit message, PR) mixed the
functional contract with style guidance in one block, and project-owner
instructions from paseo.json were appended after the defaults with an
"override where they conflict" notice — so custom wording competed with the
built-in style instead of replacing it.

Each prompt is now a non-overridable contract (what to produce, the JSON
shape, correctness/safety rules) plus style slots that paseo.json
instructions replace wholesale. Title and branch style are now separate
keys (metadataGeneration.title vs .branchName); previously branchName
instructions also shaped the title.
2026-06-17 16:48:24 +07:00

65 lines
2.3 KiB
TypeScript

import { readPaseoConfigJson } from "./paseo-config-file.js";
import {
PaseoConfigSchema,
type PaseoMetadataGeneration,
} from "@getpaseo/protocol/paseo-config-schema";
export type MetadataConfigKey = "title" | "branchName" | "commitMessage" | "pullRequest";
export interface RepoRootResolver {
resolveRepoRoot: (cwd: string) => Promise<string>;
}
// A style section carries the default guidance for one artifact. The project
// owner replaces it wholesale via paseo.json metadataGeneration.<configKey>.instructions
// — their text is used instead of the default, never appended alongside it, so the
// two never conflict. The contract block (what to produce, the JSON shape, and any
// correctness/safety rules) lives outside the sections and is never overridable.
export interface MetadataStyleSection {
configKey: MetadataConfigKey;
default: string;
label?: string;
}
export interface BuildMetadataPromptOptions {
cwd: string;
contract: string;
styles: MetadataStyleSection[];
after: string;
trailing?: string;
workspaceGitService?: RepoRootResolver;
}
export async function buildMetadataPrompt(options: BuildMetadataPromptOptions): Promise<string> {
const overrides = await readProjectMetadataOverrides(options);
const styleBlocks = options.styles.map((section) =>
renderStyleSection(section, overrides?.[section.configKey]?.instructions),
);
const head = [options.contract, ...styleBlocks, options.after].join("\n\n");
return options.trailing ? `${head}\n\n${options.trailing}` : head;
}
function renderStyleSection(section: MetadataStyleSection, override: string | undefined): string {
const body = isNonEmptyString(override) ? override.trim() : section.default;
return section.label ? `${section.label}:\n${body}` : body;
}
async function readProjectMetadataOverrides(
options: Pick<BuildMetadataPromptOptions, "cwd" | "workspaceGitService">,
): Promise<PaseoMetadataGeneration | undefined> {
if (!options.workspaceGitService) {
return undefined;
}
try {
const repoRoot = await options.workspaceGitService.resolveRepoRoot(options.cwd);
const json = readPaseoConfigJson(repoRoot);
return PaseoConfigSchema.parse(json).metadataGeneration;
} catch {
return undefined;
}
}
function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.trim() !== "";
}