@shahmarasy/prodo 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents.js +4 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +265 -31
- package/dist/cli.js +80 -3
- package/dist/init-tui.d.ts +3 -0
- package/dist/init-tui.js +28 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +9 -3
- package/dist/normalize.js +55 -7
- package/dist/providers/openai-provider.js +2 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +11 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/validator.js +0 -4
- package/dist/workflow-commands.js +2 -1
- package/package.json +1 -1
- package/presets/fintech/preset.json +48 -1
- package/presets/fintech/prompts/prd.md +99 -2
- package/presets/marketplace/preset.json +51 -1
- package/presets/marketplace/prompts/prd.md +140 -2
- package/presets/saas/preset.json +53 -1
- package/presets/saas/prompts/prd.md +150 -2
- package/src/agents.ts +4 -2
- package/src/artifacts.ts +323 -28
- package/src/cli.ts +97 -6
- package/src/init-tui.ts +30 -1
- package/src/init.ts +11 -4
- package/src/normalize.ts +55 -7
- package/src/providers/openai-provider.ts +2 -1
- package/src/settings.ts +3 -2
- package/src/templates.ts +12 -0
- package/src/utils.ts +14 -0
- package/src/validator.ts +0 -4
- package/src/workflow-commands.ts +2 -1
- package/templates/commands/prodo-fix.md +46 -0
- package/templates/commands/prodo-normalize.md +116 -14
- package/templates/commands/prodo-prd.md +136 -12
- package/templates/commands/prodo-stories.md +151 -12
- package/templates/commands/prodo-techspec.md +165 -12
- package/templates/commands/prodo-validate.md +184 -23
- package/templates/commands/prodo-wireframe.md +186 -12
- package/templates/commands/prodo-workflow.md +198 -12
package/src/artifacts.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { extractRequiredHeadingsFromTemplate, resolveCompanionTemplate, resolveT
|
|
|
19
19
|
import { readSettings } from "./settings";
|
|
20
20
|
import { sectionTextMap } from "./markdown";
|
|
21
21
|
import type { ArtifactDoc, ArtifactType, ContractCoverage } from "./types";
|
|
22
|
-
import { fileExists, isPathInside, listFilesSortedByMtime, readJsonFile, timestampSlug } from "./utils";
|
|
22
|
+
import { artifactFileStamp, fileExists, isPathInside, listFilesSortedByMtime, readJsonFile, timestampSlug } from "./utils";
|
|
23
23
|
import { validateSchema } from "./validator";
|
|
24
24
|
|
|
25
25
|
export type GenerateOptions = {
|
|
@@ -28,12 +28,11 @@ export type GenerateOptions = {
|
|
|
28
28
|
normalizedBriefOverride?: string;
|
|
29
29
|
outPath?: string;
|
|
30
30
|
agent?: string;
|
|
31
|
+
revisionType?: "default" | "fix";
|
|
31
32
|
};
|
|
32
33
|
|
|
33
34
|
function defaultFilename(type: ArtifactType): string {
|
|
34
|
-
|
|
35
|
-
if (type === "wireframe") return `${type}-${timestampSlug()}.md`;
|
|
36
|
-
return `${type}-${timestampSlug()}.md`;
|
|
35
|
+
return `${type}-${artifactFileStamp()}.md`;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
function sidecarPath(filePath: string): string {
|
|
@@ -179,12 +178,196 @@ function renderWorkflowMermaidTemplate(
|
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
180
|
|
|
181
|
+
function normalizeAuthor(author: string | undefined): string | undefined {
|
|
182
|
+
if (!author) return undefined;
|
|
183
|
+
const normalized = author.trim();
|
|
184
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function replaceAuthorPlaceholders(body: string, author: string | undefined): string {
|
|
188
|
+
const safeAuthor = normalizeAuthor(author);
|
|
189
|
+
if (!safeAuthor) return body;
|
|
190
|
+
return body.replace(/\{\{\s*author\s*\}\}/gi, safeAuthor);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function todayYmd(): string {
|
|
194
|
+
const now = new Date();
|
|
195
|
+
const y = now.getFullYear();
|
|
196
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
197
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
198
|
+
return `${y}-${m}-${d}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function headingKey(value: string): string {
|
|
202
|
+
return value
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
205
|
+
.trim();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function defaultDocumentControlValues(
|
|
209
|
+
lang: string,
|
|
210
|
+
revisionType: "default" | "fix",
|
|
211
|
+
version: string,
|
|
212
|
+
author?: string
|
|
213
|
+
): { version: string; date: string; author: string; description: string } {
|
|
214
|
+
const tr = lang.toLowerCase().startsWith("tr");
|
|
215
|
+
const safeAuthor = normalizeAuthor(author) ?? (tr ? "Prodo" : "Prodo");
|
|
216
|
+
const description = revisionType === "fix"
|
|
217
|
+
? (tr ? "Dogrulama sonrasi duzeltme revizyonu" : "Post-validation fix revision")
|
|
218
|
+
: (tr ? "Ilk surum" : "Initial version");
|
|
219
|
+
return {
|
|
220
|
+
version,
|
|
221
|
+
date: todayYmd(),
|
|
222
|
+
author: safeAuthor,
|
|
223
|
+
description
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyDocumentControlDefaults(
|
|
228
|
+
body: string,
|
|
229
|
+
options: { lang: string; revisionType: "default" | "fix"; version: string; author?: string }
|
|
230
|
+
): string {
|
|
231
|
+
const defaults = defaultDocumentControlValues(options.lang, options.revisionType, options.version, options.author);
|
|
232
|
+
let out = body
|
|
233
|
+
.replace(/\{\{\s*date\s*\}\}/gi, defaults.date)
|
|
234
|
+
.replace(/\{\{\s*description\s*\}\}/gi, defaults.description)
|
|
235
|
+
.replace(/\{\{\s*version\s*\}\}/gi, defaults.version);
|
|
236
|
+
|
|
237
|
+
const lines = out.split(/\r?\n/);
|
|
238
|
+
const headingIndex = lines.findIndex((line) => {
|
|
239
|
+
const match = line.match(/^\s*##+\s+(.+?)\s*$/);
|
|
240
|
+
if (!match) return false;
|
|
241
|
+
const key = headingKey(match[1]);
|
|
242
|
+
return key.includes("document control") || key.includes("belge kontrol");
|
|
243
|
+
});
|
|
244
|
+
if (headingIndex === -1) return out;
|
|
245
|
+
|
|
246
|
+
const row = `| ${defaults.version} | ${defaults.date} | ${defaults.author} | ${defaults.description} |`;
|
|
247
|
+
let tableSeparatorIndex = -1;
|
|
248
|
+
let tableDataIndex = -1;
|
|
249
|
+
|
|
250
|
+
for (let i = headingIndex + 1; i < lines.length; i += 1) {
|
|
251
|
+
if (/^\s*##+\s+/.test(lines[i])) break;
|
|
252
|
+
if (tableSeparatorIndex === -1 && /\|/.test(lines[i]) && /-/.test(lines[i])) {
|
|
253
|
+
tableSeparatorIndex = i;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (tableSeparatorIndex !== -1 && /^\s*\|/.test(lines[i])) {
|
|
257
|
+
tableDataIndex = i;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (tableDataIndex !== -1) {
|
|
263
|
+
lines[tableDataIndex] = row;
|
|
264
|
+
} else if (tableSeparatorIndex !== -1) {
|
|
265
|
+
lines.splice(tableSeparatorIndex + 1, 0, row);
|
|
266
|
+
} else {
|
|
267
|
+
lines.splice(headingIndex + 1, 0, "", "| Version | Date | Author | Description |", "|--------|------|--------|-------------|", row, "");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
out = lines.join("\n");
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseVersionToken(input: string): { major: number; minor: number } | null {
|
|
275
|
+
const match = input.match(/v?\s*(\d+)(?:\.(\d+))?/i);
|
|
276
|
+
if (!match) return null;
|
|
277
|
+
const major = Number(match[1]);
|
|
278
|
+
const minor = Number(match[2] ?? "0");
|
|
279
|
+
if (!Number.isFinite(major) || !Number.isFinite(minor)) return null;
|
|
280
|
+
return { major, minor };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractDocumentControlVersion(body: string): string | undefined {
|
|
284
|
+
const tableMatch = body.match(/\|\s*(v?\d+(?:\.\d+)?)\s*\|/i);
|
|
285
|
+
if (tableMatch?.[1]) return tableMatch[1].trim().startsWith("v") ? tableMatch[1].trim() : `v${tableMatch[1].trim()}`;
|
|
286
|
+
const looseMatch = body.match(/\bv?\d+\.\d+\b/i);
|
|
287
|
+
if (looseMatch?.[0]) return looseMatch[0].startsWith("v") ? looseMatch[0] : `v${looseMatch[0]}`;
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function resolveDocumentControlVersion(
|
|
292
|
+
cwd: string,
|
|
293
|
+
artifactType: ArtifactType,
|
|
294
|
+
revisionType: "default" | "fix"
|
|
295
|
+
): Promise<string> {
|
|
296
|
+
if (revisionType !== "fix") return "v1.0";
|
|
297
|
+
|
|
298
|
+
const activePath = await getActiveArtifactPath(cwd, artifactType);
|
|
299
|
+
const fallbackPath = activePath ?? (await loadLatestArtifactPath(cwd, artifactType));
|
|
300
|
+
if (!fallbackPath || !(await fileExists(fallbackPath))) {
|
|
301
|
+
return "v1.1";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const previous = await loadArtifactDoc(fallbackPath);
|
|
306
|
+
const previousVersion = extractDocumentControlVersion(previous.body) ?? String(previous.frontmatter.version ?? "");
|
|
307
|
+
const parsed = parseVersionToken(previousVersion);
|
|
308
|
+
if (!parsed) return "v1.1";
|
|
309
|
+
return `v${parsed.major}.${parsed.minor + 1}`;
|
|
310
|
+
} catch {
|
|
311
|
+
return "v1.1";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function enforceAuthorInControlTables(body: string, author: string | undefined): string {
|
|
316
|
+
const safeAuthor = normalizeAuthor(author);
|
|
317
|
+
if (!safeAuthor) return body;
|
|
318
|
+
return body.replace(
|
|
319
|
+
/(\|\s*v?[0-9.]+\s*\|\s*[^|]*\|\s*)([^|]*)(\|\s*[^|]*\|)/gi,
|
|
320
|
+
(_match, left: string, _current: string, right: string) => `${left}${safeAuthor} ${right}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function resolveUniqueOutputPath(targetDir: string, fileName: string): Promise<string> {
|
|
325
|
+
const parsed = path.parse(fileName);
|
|
326
|
+
let candidate = path.join(targetDir, fileName);
|
|
327
|
+
let index = 2;
|
|
328
|
+
while (await fileExists(candidate)) {
|
|
329
|
+
candidate = path.join(targetDir, `${parsed.name}-${String(index).padStart(2, "0")}${parsed.ext}`);
|
|
330
|
+
index += 1;
|
|
331
|
+
}
|
|
332
|
+
return candidate;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function workflowFeatureTargets(
|
|
336
|
+
normalized: NormalizedBrief,
|
|
337
|
+
coverage: ContractCoverage
|
|
338
|
+
): Array<{ id: string; text: string }> {
|
|
339
|
+
const byId = new Map(normalized.contracts.core_features.map((item) => [item.id, item]));
|
|
340
|
+
const explicit = coverage.core_features
|
|
341
|
+
.map((id) => byId.get(id))
|
|
342
|
+
.filter((item): item is { id: string; text: string } => Boolean(item));
|
|
343
|
+
|
|
344
|
+
if (explicit.length > 1) return explicit;
|
|
345
|
+
if (normalized.contracts.core_features.length > 1) return normalized.contracts.core_features.slice(0, 6);
|
|
346
|
+
if (explicit.length === 1) return explicit;
|
|
347
|
+
return normalized.contracts.core_features.slice(0, 1);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function renderWorkflowMarkdownForFeature(
|
|
351
|
+
markdown: string,
|
|
352
|
+
feature: { id: string; text: string },
|
|
353
|
+
lang: string
|
|
354
|
+
): string {
|
|
355
|
+
const tr = lang.toLowerCase().startsWith("tr");
|
|
356
|
+
const noteHeading = tr ? "## Akis Odagi" : "## Flow Focus";
|
|
357
|
+
const noteLine = tr
|
|
358
|
+
? `- [${feature.id}] Bu akis ${feature.text} ihtiyacina odaklanir.`
|
|
359
|
+
: `- [${feature.id}] This flow focuses on ${feature.text}.`;
|
|
360
|
+
if (markdown.includes(noteHeading)) return markdown;
|
|
361
|
+
return `${markdown.trim()}\n\n${noteHeading}\n${noteLine}`.trim();
|
|
362
|
+
}
|
|
363
|
+
|
|
182
364
|
async function resolvePrompt(
|
|
183
365
|
cwd: string,
|
|
184
366
|
artifactType: ArtifactType,
|
|
185
367
|
templateContent: string,
|
|
186
368
|
requiredHeadings: string[],
|
|
187
369
|
companionTemplate: { path: string; content: string } | null,
|
|
370
|
+
outputAuthor: string | undefined,
|
|
188
371
|
agent?: string
|
|
189
372
|
): Promise<string> {
|
|
190
373
|
const base = await fs.readFile(promptPath(cwd, artifactType), "utf8");
|
|
@@ -228,12 +411,19 @@ Wireframe paired output contract (STRICT):
|
|
|
228
411
|
- Generate companion HTML screens based on native wireframe template.
|
|
229
412
|
- HTML must stay low-fidelity and structure-first.`
|
|
230
413
|
: "";
|
|
414
|
+
const authorPolicy = outputAuthor && outputAuthor.trim().length > 0
|
|
415
|
+
? `
|
|
416
|
+
Author policy (STRICT):
|
|
417
|
+
- Use this exact author name wherever author is required: ${outputAuthor.trim()}
|
|
418
|
+
- Do not invent random author names.`
|
|
419
|
+
: "";
|
|
231
420
|
const withTemplate = `${base}
|
|
232
421
|
|
|
233
422
|
${authority}
|
|
234
423
|
${companionAuthority}
|
|
235
424
|
${workflowPairing}
|
|
236
|
-
${wireframePairing}
|
|
425
|
+
${wireframePairing}
|
|
426
|
+
${authorPolicy}`;
|
|
237
427
|
if (!agent) return withTemplate;
|
|
238
428
|
return `${withTemplate}
|
|
239
429
|
|
|
@@ -428,6 +618,62 @@ function splitWorkflowPair(raw: string): { markdown: string; mermaid: string } {
|
|
|
428
618
|
return { markdown, mermaid };
|
|
429
619
|
}
|
|
430
620
|
|
|
621
|
+
async function writeWorkflowFlows(
|
|
622
|
+
targetDir: string,
|
|
623
|
+
baseName: string,
|
|
624
|
+
normalized: NormalizedBrief,
|
|
625
|
+
coverage: ContractCoverage,
|
|
626
|
+
lang: string,
|
|
627
|
+
markdownBody: string,
|
|
628
|
+
mermaidBody: string | null,
|
|
629
|
+
mermaidTemplateContent: string | null
|
|
630
|
+
): Promise<{ primaryPath: string; summaryBody: string; rendered: Array<{ mdPath: string; body: string }> }> {
|
|
631
|
+
const targets = workflowFeatureTargets(normalized, coverage);
|
|
632
|
+
const fallbackFeature = normalized.contracts.core_features[0] ?? { id: "F1", text: "Primary flow" };
|
|
633
|
+
const flows = targets.length > 0 ? targets : [fallbackFeature];
|
|
634
|
+
const summaryBodies: string[] = [];
|
|
635
|
+
const renderedArtifacts: Array<{ mdPath: string; body: string }> = [];
|
|
636
|
+
let primaryMdPath = "";
|
|
637
|
+
|
|
638
|
+
for (const [index, flowFeature] of flows.entries()) {
|
|
639
|
+
const flowBase =
|
|
640
|
+
flows.length === 1
|
|
641
|
+
? baseName
|
|
642
|
+
: (index === 0
|
|
643
|
+
? baseName
|
|
644
|
+
: `${baseName}-${index + 1}-${toSlug(extractTurkishTitle(flowFeature.text))}`);
|
|
645
|
+
const mdPath = path.join(targetDir, `${flowBase}.md`);
|
|
646
|
+
const mmdPath = path.join(targetDir, `${flowBase}.mmd`);
|
|
647
|
+
const featureCoverage: ContractCoverage = {
|
|
648
|
+
...coverage,
|
|
649
|
+
core_features: [flowFeature.id]
|
|
650
|
+
};
|
|
651
|
+
const renderedMarkdown = renderWorkflowMarkdownForFeature(markdownBody, flowFeature, lang);
|
|
652
|
+
const renderedMermaid = (mermaidTemplateContent && mermaidTemplateContent.trim().length > 0)
|
|
653
|
+
? renderWorkflowMermaidTemplate(mermaidTemplateContent, normalized, featureCoverage, lang).trim()
|
|
654
|
+
: (mermaidBody ?? "").trim();
|
|
655
|
+
|
|
656
|
+
if (!/(^|\n)\s*(flowchart|graph)\s+/i.test(renderedMermaid)) {
|
|
657
|
+
throw new UserError("Workflow Mermaid output is invalid.");
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
enforceLanguage(renderedMarkdown, lang, "workflow");
|
|
661
|
+
enforceLanguage(renderedMermaid, lang, "workflow");
|
|
662
|
+
await fs.writeFile(mdPath, `${renderedMarkdown}\n`, "utf8");
|
|
663
|
+
await fs.writeFile(mmdPath, `${renderedMermaid}\n`, "utf8");
|
|
664
|
+
|
|
665
|
+
if (!primaryMdPath) primaryMdPath = mdPath;
|
|
666
|
+
summaryBodies.push(renderedMarkdown);
|
|
667
|
+
renderedArtifacts.push({ mdPath, body: renderedMarkdown });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
primaryPath: primaryMdPath,
|
|
672
|
+
summaryBody: summaryBodies.join("\n\n"),
|
|
673
|
+
rendered: renderedArtifacts
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
431
677
|
async function writeWireframeScreens(
|
|
432
678
|
targetDir: string,
|
|
433
679
|
baseName: string,
|
|
@@ -438,10 +684,17 @@ async function writeWireframeScreens(
|
|
|
438
684
|
htmlTemplateContent: string | null
|
|
439
685
|
): Promise<{ primaryPath: string; summaryBody: string }> {
|
|
440
686
|
const tr = lang.toLowerCase().startsWith("tr");
|
|
441
|
-
const
|
|
687
|
+
const explicitScreens = normalized.contracts.core_features
|
|
442
688
|
.filter((item) => coverage.core_features.includes(item.id))
|
|
443
689
|
.slice(0, 6);
|
|
444
|
-
const screens =
|
|
690
|
+
const screens =
|
|
691
|
+
explicitScreens.length > 1
|
|
692
|
+
? explicitScreens
|
|
693
|
+
: (normalized.contracts.core_features.length > 1
|
|
694
|
+
? normalized.contracts.core_features.slice(0, 6)
|
|
695
|
+
: (explicitScreens.length === 1
|
|
696
|
+
? explicitScreens
|
|
697
|
+
: normalized.contracts.core_features.slice(0, 1)));
|
|
445
698
|
const summaryBodies: string[] = [];
|
|
446
699
|
let primaryMdPath = "";
|
|
447
700
|
for (const [index, screen] of screens.entries()) {
|
|
@@ -568,9 +821,11 @@ async function writeWireframeScreens(
|
|
|
568
821
|
|
|
569
822
|
export async function generateArtifact(options: GenerateOptions): Promise<string> {
|
|
570
823
|
const { cwd, artifactType, outPath, agent } = options;
|
|
824
|
+
const revisionType = options.revisionType ?? "default";
|
|
571
825
|
const def = await getArtifactDefinition(cwd, artifactType);
|
|
572
826
|
const normalizedPath = options.normalizedBriefOverride ?? normalizedBriefPath(cwd);
|
|
573
827
|
await ensurePipelinePrereqs(cwd, normalizedPath);
|
|
828
|
+
const documentControlVersion = await resolveDocumentControlVersion(cwd, artifactType, revisionType);
|
|
574
829
|
|
|
575
830
|
const settings = await readSettings(cwd);
|
|
576
831
|
const normalizedBriefRaw = await readJsonFile<Record<string, unknown>>(normalizedPath);
|
|
@@ -608,6 +863,7 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
608
863
|
template?.content ?? "",
|
|
609
864
|
computedHeadings,
|
|
610
865
|
companionTemplate,
|
|
866
|
+
settings.author,
|
|
611
867
|
agent
|
|
612
868
|
);
|
|
613
869
|
const provider = createProvider();
|
|
@@ -628,17 +884,24 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
628
884
|
templatePath: template?.path ?? "",
|
|
629
885
|
companionTemplateContent: companionTemplate?.content ?? "",
|
|
630
886
|
companionTemplatePath: companionTemplate?.path ?? "",
|
|
631
|
-
outputLanguage: settings.lang
|
|
887
|
+
outputLanguage: settings.lang,
|
|
888
|
+
outputAuthor: settings.author
|
|
632
889
|
},
|
|
633
890
|
schemaHint
|
|
634
891
|
);
|
|
635
892
|
|
|
636
|
-
let generatedBody =
|
|
893
|
+
let generatedBody = enforceAuthorInControlTables(
|
|
894
|
+
replaceAuthorPlaceholders(generated.body.trim(), settings.author),
|
|
895
|
+
settings.author
|
|
896
|
+
);
|
|
637
897
|
let workflowMermaidBody: string | null = null;
|
|
638
898
|
if (artifactType === "workflow") {
|
|
639
899
|
const paired = splitWorkflowPair(generatedBody);
|
|
640
|
-
generatedBody =
|
|
641
|
-
|
|
900
|
+
generatedBody = enforceAuthorInControlTables(
|
|
901
|
+
replaceAuthorPlaceholders(paired.markdown, settings.author),
|
|
902
|
+
settings.author
|
|
903
|
+
);
|
|
904
|
+
workflowMermaidBody = replaceAuthorPlaceholders(paired.mermaid, settings.author);
|
|
642
905
|
}
|
|
643
906
|
let contractCoverage = extractCoverageFromBody(generatedBody);
|
|
644
907
|
|
|
@@ -660,6 +923,13 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
660
923
|
}
|
|
661
924
|
}
|
|
662
925
|
|
|
926
|
+
generatedBody = applyDocumentControlDefaults(generatedBody, {
|
|
927
|
+
lang: settings.lang,
|
|
928
|
+
revisionType,
|
|
929
|
+
version: documentControlVersion,
|
|
930
|
+
author: settings.author
|
|
931
|
+
});
|
|
932
|
+
|
|
663
933
|
if (artifactType === "workflow" && companionTemplate?.content) {
|
|
664
934
|
workflowMermaidBody = renderWorkflowMermaidTemplate(
|
|
665
935
|
companionTemplate.content,
|
|
@@ -667,6 +937,7 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
667
937
|
contractCoverage,
|
|
668
938
|
settings.lang
|
|
669
939
|
).trim();
|
|
940
|
+
workflowMermaidBody = replaceAuthorPlaceholders(workflowMermaidBody, settings.author);
|
|
670
941
|
}
|
|
671
942
|
|
|
672
943
|
enforceLanguage(generatedBody, settings.lang, artifactType);
|
|
@@ -691,10 +962,14 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
691
962
|
status: DEFAULT_STATUS,
|
|
692
963
|
upstream_artifacts: upstreamArtifacts.map((item) => item.file),
|
|
693
964
|
contract_coverage: contractCoverage,
|
|
694
|
-
language: settings.lang
|
|
965
|
+
language: settings.lang,
|
|
966
|
+
...(normalizeAuthor(settings.author) ? { author: normalizeAuthor(settings.author) } : {})
|
|
695
967
|
} as Record<string, unknown>;
|
|
696
968
|
|
|
697
969
|
const mergedFrontmatter = { ...frontmatter, ...(generated.frontmatter ?? {}) };
|
|
970
|
+
if (normalizeAuthor(settings.author)) {
|
|
971
|
+
mergedFrontmatter.author = normalizeAuthor(settings.author);
|
|
972
|
+
}
|
|
698
973
|
let doc: ArtifactDoc = {
|
|
699
974
|
frontmatter: mergedFrontmatter,
|
|
700
975
|
body: generatedBody
|
|
@@ -708,29 +983,49 @@ export async function generateArtifact(options: GenerateOptions): Promise<string
|
|
|
708
983
|
}
|
|
709
984
|
|
|
710
985
|
const targetDir = outputDirPath(cwd, artifactType, def.output_dir);
|
|
711
|
-
const finalPath = outPath
|
|
986
|
+
const finalPath = outPath
|
|
987
|
+
? path.resolve(cwd, outPath)
|
|
988
|
+
: await resolveUniqueOutputPath(targetDir, defaultFilename(artifactType));
|
|
712
989
|
if (!isPathInside(path.join(cwd, "product-docs"), finalPath)) {
|
|
713
990
|
throw new UserError("Artifact output must be inside `product-docs/`.");
|
|
714
991
|
}
|
|
715
992
|
await fs.mkdir(path.dirname(finalPath), { recursive: true });
|
|
716
993
|
if (artifactType === "workflow") {
|
|
717
994
|
const basePath = path.join(path.dirname(finalPath), path.parse(finalPath).name);
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
...deriveStructuredContext(artifactType, doc.body, schemaHint.requiredHeadings)
|
|
729
|
-
};
|
|
995
|
+
const workflow = await writeWorkflowFlows(
|
|
996
|
+
path.dirname(basePath),
|
|
997
|
+
path.parse(basePath).name,
|
|
998
|
+
normalizedBrief,
|
|
999
|
+
contractCoverage,
|
|
1000
|
+
settings.lang,
|
|
1001
|
+
doc.body,
|
|
1002
|
+
workflowMermaidBody,
|
|
1003
|
+
companionTemplate?.content ?? null
|
|
1004
|
+
);
|
|
730
1005
|
await fs.mkdir(outputContextDirPath(cwd), { recursive: true });
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
1006
|
+
for (const rendered of workflow.rendered) {
|
|
1007
|
+
const renderedDoc: ArtifactDoc = {
|
|
1008
|
+
frontmatter: doc.frontmatter,
|
|
1009
|
+
body: rendered.body
|
|
1010
|
+
};
|
|
1011
|
+
await fs.writeFile(rendered.mdPath, matter.stringify(renderedDoc.body, renderedDoc.frontmatter), "utf8");
|
|
1012
|
+
await writeSidecar(rendered.mdPath, renderedDoc);
|
|
1013
|
+
const renderedContext = {
|
|
1014
|
+
artifact_type: artifactType,
|
|
1015
|
+
artifact_file: rendered.mdPath,
|
|
1016
|
+
generated_at: new Date().toISOString(),
|
|
1017
|
+
contract_coverage: contractCoverage,
|
|
1018
|
+
...deriveStructuredContext(artifactType, renderedDoc.body, schemaHint.requiredHeadings)
|
|
1019
|
+
};
|
|
1020
|
+
await fs.writeFile(contextFilePath(cwd, rendered.mdPath), `${JSON.stringify(renderedContext, null, 2)}\n`, "utf8");
|
|
1021
|
+
}
|
|
1022
|
+
const primaryRendered = workflow.rendered.find((item) => item.mdPath === workflow.primaryPath) ?? workflow.rendered[0];
|
|
1023
|
+
doc = {
|
|
1024
|
+
frontmatter: doc.frontmatter,
|
|
1025
|
+
body: primaryRendered?.body ?? doc.body
|
|
1026
|
+
};
|
|
1027
|
+
await setActiveArtifact(cwd, artifactType, workflow.primaryPath);
|
|
1028
|
+
return workflow.primaryPath;
|
|
734
1029
|
} else if (artifactType === "wireframe") {
|
|
735
1030
|
const base = path.parse(finalPath).name;
|
|
736
1031
|
const wireframe = await writeWireframeScreens(
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { loadAgentCommandSet, resolveAgent } from "./agents";
|
|
6
6
|
import { resolveAi, type SupportedAi } from "./agent-command-installer";
|
|
7
|
-
import { listArtifactTypes } from "./artifact-registry";
|
|
7
|
+
import { listArtifactDefinitions, listArtifactTypes } from "./artifact-registry";
|
|
8
8
|
import { generateArtifact } from "./artifacts";
|
|
9
9
|
import { runDoctor } from "./doctor";
|
|
10
10
|
import { UserError } from "./errors";
|
|
@@ -30,10 +30,11 @@ const dynamicImport = new Function("specifier", "return import(specifier)") as (
|
|
|
30
30
|
specifier: string
|
|
31
31
|
) => Promise<unknown>;
|
|
32
32
|
|
|
33
|
-
function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | undefined {
|
|
33
|
+
function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | "fix" | undefined {
|
|
34
34
|
if (forcedCommand === "prodo-init") return "init";
|
|
35
35
|
if (forcedCommand === "prodo-validate") return "validate";
|
|
36
36
|
if (forcedCommand === "prodo-normalize") return "normalize";
|
|
37
|
+
if (forcedCommand === "prodo-fix") return "fix";
|
|
37
38
|
if (forcedCommand === "prodo-prd") return "prd";
|
|
38
39
|
if (forcedCommand === "prodo-workflow") return "workflow";
|
|
39
40
|
if (forcedCommand === "prodo-wireframe") return "wireframe";
|
|
@@ -44,7 +45,7 @@ function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "valid
|
|
|
44
45
|
|
|
45
46
|
async function runArtifactCommand(
|
|
46
47
|
type: ArtifactType,
|
|
47
|
-
opts: { from?: string; out?: string; agent?: string },
|
|
48
|
+
opts: { from?: string; out?: string; agent?: string; revisionType?: "default" | "fix" },
|
|
48
49
|
cwd: string,
|
|
49
50
|
log: (message: string) => void,
|
|
50
51
|
options?: { suggestValidate?: boolean }
|
|
@@ -56,7 +57,8 @@ async function runArtifactCommand(
|
|
|
56
57
|
cwd,
|
|
57
58
|
normalizedBriefOverride: opts.from,
|
|
58
59
|
outPath: opts.out,
|
|
59
|
-
agent
|
|
60
|
+
agent,
|
|
61
|
+
revisionType: opts.revisionType
|
|
60
62
|
});
|
|
61
63
|
const agentMsg = agent ? ` [agent=${agent}]` : "";
|
|
62
64
|
log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
|
|
@@ -116,13 +118,15 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
116
118
|
.command("init [target]")
|
|
117
119
|
.option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
|
|
118
120
|
.option("--lang <code>", "document language (e.g. en, tr)")
|
|
121
|
+
.option("--author <name>", "document author name")
|
|
119
122
|
.option("--preset <name>", "preset to install during initialization")
|
|
120
123
|
.action(async (target, opts) => {
|
|
121
124
|
const projectRoot = path.resolve(cwd, target ?? ".");
|
|
122
125
|
const selected = await gatherInitSelections({
|
|
123
126
|
projectRoot,
|
|
124
127
|
aiInput: opts.ai,
|
|
125
|
-
langInput: opts.lang
|
|
128
|
+
langInput: opts.lang,
|
|
129
|
+
authorInput: opts.author
|
|
126
130
|
});
|
|
127
131
|
const selectedAi = selected.ai as SupportedAi | undefined;
|
|
128
132
|
|
|
@@ -133,6 +137,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
133
137
|
const result = await runInit(projectRoot, {
|
|
134
138
|
ai: selectedAi,
|
|
135
139
|
lang: selected.lang,
|
|
140
|
+
author: selected.author,
|
|
136
141
|
preset: opts.preset,
|
|
137
142
|
script: selected.script
|
|
138
143
|
});
|
|
@@ -141,7 +146,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
141
146
|
projectRoot,
|
|
142
147
|
settingsPath: result.settingsPath,
|
|
143
148
|
ai: selectedAi,
|
|
144
|
-
lang: selected.lang
|
|
149
|
+
lang: selected.lang,
|
|
150
|
+
author: selected.author
|
|
145
151
|
});
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
@@ -149,6 +155,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
149
155
|
const result = await runInit(projectRoot, {
|
|
150
156
|
ai: selectedAi,
|
|
151
157
|
lang: selected.lang,
|
|
158
|
+
author: selected.author,
|
|
152
159
|
preset: opts.preset,
|
|
153
160
|
script: selected.script
|
|
154
161
|
});
|
|
@@ -161,6 +168,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
161
168
|
out("No agent selected. Use `prodo generate` for end-to-end generation.");
|
|
162
169
|
}
|
|
163
170
|
out(`Settings file: ${result.settingsPath}`);
|
|
171
|
+
out(`Author: ${selected.author}`);
|
|
164
172
|
out("Next: edit brief.md.");
|
|
165
173
|
});
|
|
166
174
|
|
|
@@ -198,6 +206,56 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
198
206
|
});
|
|
199
207
|
});
|
|
200
208
|
|
|
209
|
+
program
|
|
210
|
+
.command("fix", { hidden: true })
|
|
211
|
+
.description("Advanced: auto-regenerate affected artifacts from validation findings")
|
|
212
|
+
.option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
|
|
213
|
+
.option("--strict", "treat validation warnings as errors")
|
|
214
|
+
.option("--report <path>", "validation report output path")
|
|
215
|
+
.action(async (opts) => {
|
|
216
|
+
if (opts.agent) resolveAgent(opts.agent);
|
|
217
|
+
await withBriefReadOnlyGuard(cwd, async () => {
|
|
218
|
+
await runHookPhase(cwd, "before_validate", out);
|
|
219
|
+
const initial = await runValidate(cwd, {
|
|
220
|
+
strict: Boolean(opts.strict),
|
|
221
|
+
report: opts.report
|
|
222
|
+
});
|
|
223
|
+
out(`Validation report written to: ${initial.reportPath}`);
|
|
224
|
+
await runHookPhase(cwd, "after_validate", out);
|
|
225
|
+
|
|
226
|
+
if (initial.pass) {
|
|
227
|
+
out("No blocking issues found. Nothing to fix.");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const targets = await resolveFixTargets(cwd, artifactTypes, initial.issues);
|
|
232
|
+
out(`Validation failed. Regenerating impacted artifacts: ${targets.join(", ")}`);
|
|
233
|
+
|
|
234
|
+
await runHookPhase(cwd, "before_normalize", out);
|
|
235
|
+
const normalizedPath = await runNormalize({ cwd });
|
|
236
|
+
out(`Normalized brief refreshed: ${normalizedPath}`);
|
|
237
|
+
await runHookPhase(cwd, "after_normalize", out);
|
|
238
|
+
|
|
239
|
+
for (const type of targets) {
|
|
240
|
+
await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent, revisionType: "fix" }, cwd, out, {
|
|
241
|
+
suggestValidate: false
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await runHookPhase(cwd, "before_validate", out);
|
|
246
|
+
const final = await runValidate(cwd, {
|
|
247
|
+
strict: Boolean(opts.strict),
|
|
248
|
+
report: opts.report
|
|
249
|
+
});
|
|
250
|
+
out(`Validation report written to: ${final.reportPath}`);
|
|
251
|
+
if (!final.pass) {
|
|
252
|
+
throw new UserError("Fix completed but validation is still failing. Review report and retry.");
|
|
253
|
+
}
|
|
254
|
+
out("Fix pipeline completed. Validation passed.");
|
|
255
|
+
await runHookPhase(cwd, "after_validate", out);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
201
259
|
program
|
|
202
260
|
.command("normalize", { hidden: true })
|
|
203
261
|
.description("Advanced: normalize brief without full pipeline")
|
|
@@ -294,6 +352,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
294
352
|
await program.parseAsync(["node", "prodo", "normalize", ...argv.slice(2)]);
|
|
295
353
|
} else if (forced === "validate") {
|
|
296
354
|
await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
|
|
355
|
+
} else if (forced === "fix") {
|
|
356
|
+
await program.parseAsync(["node", "prodo", "fix", ...argv.slice(2)]);
|
|
297
357
|
} else {
|
|
298
358
|
await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
|
|
299
359
|
}
|
|
@@ -317,3 +377,34 @@ if (require.main === module) {
|
|
|
317
377
|
process.exitCode = code;
|
|
318
378
|
});
|
|
319
379
|
}
|
|
380
|
+
|
|
381
|
+
async function resolveFixTargets(
|
|
382
|
+
cwd: string,
|
|
383
|
+
artifactTypes: ArtifactType[],
|
|
384
|
+
issues: Array<{ artifactType?: ArtifactType }>
|
|
385
|
+
): Promise<ArtifactType[]> {
|
|
386
|
+
const direct = new Set<ArtifactType>(
|
|
387
|
+
issues
|
|
388
|
+
.map((issue) => issue.artifactType)
|
|
389
|
+
.filter(
|
|
390
|
+
(artifactType): artifactType is ArtifactType =>
|
|
391
|
+
typeof artifactType === "string" && artifactTypes.includes(artifactType)
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
if (direct.size === 0) return artifactTypes;
|
|
395
|
+
|
|
396
|
+
const defs = await listArtifactDefinitions(cwd);
|
|
397
|
+
let changed = true;
|
|
398
|
+
while (changed) {
|
|
399
|
+
changed = false;
|
|
400
|
+
for (const def of defs) {
|
|
401
|
+
const needsRefresh = def.upstream.some((upstream) => direct.has(upstream));
|
|
402
|
+
if (needsRefresh && !direct.has(def.name)) {
|
|
403
|
+
direct.add(def.name);
|
|
404
|
+
changed = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return artifactTypes.filter((artifactType) => direct.has(artifactType));
|
|
410
|
+
}
|