feat(prd): one transitional PRD authority — RI-4-001 (#1275) (#1294)
ci/woodpecker/push/publish Pipeline was canceled

Co-authored-by: fargo <[email protected]>
This commit was merged in pull request #1294.
This commit is contained in:
2026-08-18 05:56:51 +00:00
committed by jarvis
parent d7e303d3c0
commit d92de53399
12 changed files with 1552 additions and 59 deletions
+43 -32
View File
@@ -2,8 +2,8 @@ import path from 'node:path';
import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts';
import { createPrd, savePrd } from './prd.js';
import type { CreatePrdOptions, PrdDocument } from './types.js';
import { PrdService } from './service.js';
import type { CreatePrdOptions, PrdDocument, PrdSectionPatch } from './types.js';
interface WizardAnswers {
goals: string;
@@ -11,20 +11,41 @@ interface WizardAnswers {
milestones: string;
}
function updateSectionField(doc: PrdDocument, sectionKeyword: string, value: string): void {
const section = doc.sections.find((candidate) => candidate.id.includes(sectionKeyword));
/**
* Translate wizard answers into section patches using the same keyword
* matching the wizard always used (first section whose id contains the
* keyword, then first field whose name contains it, else first field).
*/
function buildWizardPatches(doc: PrdDocument, answers: WizardAnswers): PrdSectionPatch[] {
const bySection = new Map<string, PrdSectionPatch>();
if (section === undefined) {
return;
}
const add = (keyword: string, value: string): void => {
const section = doc.sections.find((candidate) => candidate.id.includes(keyword));
if (section === undefined) {
return;
}
const fieldName =
Object.keys(section.fields).find((field) => field.toLowerCase().includes(sectionKeyword)) ??
Object.keys(section.fields)[0];
const fieldName =
Object.keys(section.fields).find((field) => field.toLowerCase().includes(keyword)) ??
Object.keys(section.fields)[0];
if (fieldName !== undefined) {
section.fields[fieldName] = value;
}
if (fieldName === undefined || section.fields[fieldName] === value) {
return;
}
const existing = bySection.get(section.id);
if (existing === undefined) {
bySection.set(section.id, { id: section.id, fields: { [fieldName]: value } });
} else {
existing.fields[fieldName] = value;
}
};
add('goal', answers.goals);
add('constraint', answers.constraints);
add('milestone', answers.milestones);
return [...bySection.values()];
}
async function promptText(message: string, initialValue = ''): Promise<string> {
@@ -63,15 +84,10 @@ async function promptTemplate(template?: string): Promise<string> {
return choice;
}
function applyWizardAnswers(doc: PrdDocument, answers: WizardAnswers): PrdDocument {
updateSectionField(doc, 'goal', answers.goals);
updateSectionField(doc, 'constraint', answers.constraints);
updateSectionField(doc, 'milestone', answers.milestones);
doc.updatedAt = new Date().toISOString();
return doc;
}
/**
* Interactive PRD wizard. All writes go through PrdService — the wizard is a
* prompt layer, never a second writer path.
*/
export async function runPrdWizard(options: CreatePrdOptions): Promise<PrdDocument> {
intro('Mosaic PRD wizard');
@@ -82,20 +98,15 @@ export async function runPrdWizard(options: CreatePrdOptions): Promise<PrdDocume
const constraints = await promptText('Key constraints');
const milestones = await promptText('Planned milestones');
const doc = await createPrd({
...options,
const service = new PrdService({ projectPath: options.projectPath });
const doc = await service.create({
name,
template,
interactive: true,
});
const updated = applyWizardAnswers(doc, {
goals,
constraints,
milestones,
});
await savePrd(updated);
const patches = buildWizardPatches(doc, { goals, constraints, milestones });
const updated =
patches.length > 0 ? await service.update({ id: doc.id, sections: patches }) : doc;
outro(`PRD created: ${path.join(updated.projectPath, 'docs', 'prdy', `${updated.id}.yaml`)}`);