feat(prdy): add PrdService as the single PRD authority surface (#1275)

- PrdService owns create/read/update/link/import/export; wizard and package
  CLI become prompt layers over the service (no second writer path)
- PRD documents gain a content version and a missions linkage array persisted
  in the YAML authority store (survives restart); linkage writes do not bump
  the content version
- exportMarkdown renders a labeled generated view (id + version + do-not-edit
  header) that no code path reads back; the store loads .yaml/.yml only
- importDocument validates structure with zod before anything else, persists
  valid imports as draft (validity is not approval), and refuses conflicts
  with a typed PrdImportConflictError carrying a proposed successor; the
  original document stays byte-identical until acceptSuccessor
- raw store writers (createPrd/savePrd) are no longer exported from the
  package entry point
This commit is contained in:
fargo
2026-08-17 16:46:00 -05:00
parent 8199261caa
commit e291bfb837
7 changed files with 1065 additions and 48 deletions
+74 -14
View File
@@ -1,6 +1,6 @@
import { Command } from 'commander';
import { createPrd, listPrds, loadPrd } from './prd.js';
import { PrdService } from './service.js';
import { runPrdWizard } from './wizard.js';
interface InitCommandOptions {
@@ -18,6 +18,22 @@ interface ShowCommandOptions {
readonly id?: string;
}
interface ImportCommandOptions {
readonly project: string;
readonly file: string;
readonly acceptSuccessor?: boolean;
}
interface ExportCommandOptions {
readonly project: string;
readonly id?: string;
readonly out?: string;
}
function serviceFor(project: string): PrdService {
return new PrdService({ projectPath: project });
}
export function buildPrdyCli(): Command {
const program = new Command();
program.name('mosaic').description('Mosaic CLI').exitOverride();
@@ -38,11 +54,9 @@ export function buildPrdyCli(): Command {
template: options.template,
interactive: true,
})
: await createPrd({
: await serviceFor(options.project).create({
name: options.name,
projectPath: options.project,
template: options.template,
interactive: false,
});
console.log(
@@ -52,6 +66,7 @@ export function buildPrdyCli(): Command {
id: doc.id,
title: doc.title,
status: doc.status,
version: doc.version,
projectPath: doc.projectPath,
},
null,
@@ -65,7 +80,7 @@ export function buildPrdyCli(): Command {
.description('List PRD documents for a project')
.requiredOption('--project <path>', 'Project path')
.action(async (options: ListCommandOptions) => {
const docs = await listPrds(options.project);
const docs = await serviceFor(options.project).list();
console.log(JSON.stringify(docs, null, 2));
});
@@ -75,20 +90,65 @@ export function buildPrdyCli(): Command {
.requiredOption('--project <path>', 'Project path')
.option('--id <id>', 'PRD document id')
.action(async (options: ShowCommandOptions) => {
if (options.id !== undefined) {
const docs = await listPrds(options.project);
const match = docs.find((doc) => doc.id === options.id);
const doc = await serviceFor(options.project).get(options.id);
console.log(JSON.stringify(doc, null, 2));
});
if (match === undefined) {
throw new Error(`PRD id not found: ${options.id}`);
}
prdy
.command('import')
.description('Import a YAML PRD document (validated; conflicts propose a successor)')
.requiredOption('--project <path>', 'Project path')
.requiredOption('--file <file>', 'Path to YAML PRD document')
.option('--accept-successor', 'Accept a conflicted import as the next version')
.action(async (options: ImportCommandOptions) => {
const service = serviceFor(options.project);
const input = { filePath: options.file };
console.log(JSON.stringify(match, null, 2));
if (options.acceptSuccessor) {
const successor = await service.acceptSuccessor(input);
console.log(
JSON.stringify(
{
ok: true,
outcome: 'successor-accepted',
id: successor.id,
version: successor.version,
},
null,
2,
),
);
return;
}
const doc = await loadPrd(options.project);
console.log(JSON.stringify(doc, null, 2));
const result = await service.importDocument(input);
console.log(
JSON.stringify(
{
ok: true,
outcome: result.kind,
id: result.document.id,
version: result.document.version,
status: result.document.status,
},
null,
2,
),
);
});
prdy
.command('export')
.description('Render a PRD to a labeled generated-view Markdown file')
.requiredOption('--project <path>', 'Project path')
.option('--id <id>', 'PRD document id')
.option('--out <path>', 'Output path (default docs/prdy/<id>.md)')
.action(async (options: ExportCommandOptions) => {
const result = await serviceFor(options.project).exportMarkdown({
id: options.id,
outPath: options.out,
});
console.log(JSON.stringify({ ok: true, filePath: result.filePath }, null, 2));
});
return program;