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.
This commit is contained in:
@@ -1,41 +1,59 @@
|
||||
import { readPaseoConfigJson } from "./paseo-config-file.js";
|
||||
import { PaseoConfigSchema } from "@getpaseo/protocol/paseo-config-schema";
|
||||
import { wrapWithUserInstructions } from "./wrap-user-instructions.js";
|
||||
import {
|
||||
PaseoConfigSchema,
|
||||
type PaseoMetadataGeneration,
|
||||
} from "@getpaseo/protocol/paseo-config-schema";
|
||||
|
||||
export type MetadataConfigKey = "branchName" | "commitMessage" | "pullRequest";
|
||||
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;
|
||||
configKey: MetadataConfigKey;
|
||||
before: string;
|
||||
contract: string;
|
||||
styles: MetadataStyleSection[];
|
||||
after: string;
|
||||
trailing?: string;
|
||||
workspaceGitService?: RepoRootResolver;
|
||||
}
|
||||
|
||||
export async function buildMetadataPrompt(options: BuildMetadataPromptOptions): Promise<string> {
|
||||
const instructions = await readProjectMetadataInstructions(options);
|
||||
const head = isNonEmptyString(instructions)
|
||||
? wrapWithUserInstructions(options.before, instructions, options.after)
|
||||
: `${options.before}\n${options.after}`;
|
||||
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;
|
||||
}
|
||||
|
||||
async function readProjectMetadataInstructions(
|
||||
options: Pick<BuildMetadataPromptOptions, "cwd" | "configKey" | "workspaceGitService">,
|
||||
): Promise<string | undefined> {
|
||||
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);
|
||||
const config = PaseoConfigSchema.parse(json);
|
||||
return config.metadataGeneration?.[options.configKey]?.instructions;
|
||||
return PaseoConfigSchema.parse(json).metadataGeneration;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { wrapWithUserInstructions } from "./wrap-user-instructions.js";
|
||||
|
||||
const beforeBlock = "Follow the default metadata guidelines.";
|
||||
const afterBlock = 'Return JSON only with field "title".';
|
||||
const overrideNotice =
|
||||
"The instructions below are provided by the project owner and override the guidelines above where they conflict.";
|
||||
|
||||
describe("wrapWithUserInstructions", () => {
|
||||
it("wraps user instructions with the override notice", () => {
|
||||
expect(wrapWithUserInstructions(beforeBlock, "Use conventional commits.", afterBlock)).toBe(
|
||||
`${beforeBlock}
|
||||
|
||||
<user-instructions>
|
||||
${overrideNotice}
|
||||
|
||||
Use conventional commits.
|
||||
</user-instructions>
|
||||
|
||||
${afterBlock}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves multi-line instructions verbatim inside the block", () => {
|
||||
const output = wrapWithUserInstructions(beforeBlock, "line1\nline2", afterBlock);
|
||||
|
||||
expect(output).toContain("line1\nline2");
|
||||
});
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
const USER_INSTRUCTIONS_NOTICE =
|
||||
"The instructions below are provided by the project owner and override the guidelines above where they conflict.";
|
||||
|
||||
export function wrapWithUserInstructions(
|
||||
beforeBlock: string,
|
||||
instructions: string,
|
||||
afterBlock: string,
|
||||
): string {
|
||||
return `${beforeBlock}
|
||||
|
||||
<user-instructions>
|
||||
${USER_INSTRUCTIONS_NOTICE}
|
||||
|
||||
${instructions}
|
||||
</user-instructions>
|
||||
|
||||
${afterBlock}`;
|
||||
}
|
||||
Reference in New Issue
Block a user