@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.
Files changed (45) hide show
  1. package/dist/agents.js +4 -2
  2. package/dist/artifacts.d.ts +1 -0
  3. package/dist/artifacts.js +265 -31
  4. package/dist/cli.js +80 -3
  5. package/dist/init-tui.d.ts +3 -0
  6. package/dist/init-tui.js +28 -1
  7. package/dist/init.d.ts +1 -0
  8. package/dist/init.js +9 -3
  9. package/dist/normalize.js +55 -7
  10. package/dist/providers/openai-provider.js +2 -1
  11. package/dist/settings.d.ts +1 -0
  12. package/dist/settings.js +2 -1
  13. package/dist/templates.d.ts +1 -1
  14. package/dist/templates.js +11 -0
  15. package/dist/utils.d.ts +1 -0
  16. package/dist/utils.js +13 -0
  17. package/dist/validator.js +0 -4
  18. package/dist/workflow-commands.js +2 -1
  19. package/package.json +1 -1
  20. package/presets/fintech/preset.json +48 -1
  21. package/presets/fintech/prompts/prd.md +99 -2
  22. package/presets/marketplace/preset.json +51 -1
  23. package/presets/marketplace/prompts/prd.md +140 -2
  24. package/presets/saas/preset.json +53 -1
  25. package/presets/saas/prompts/prd.md +150 -2
  26. package/src/agents.ts +4 -2
  27. package/src/artifacts.ts +323 -28
  28. package/src/cli.ts +97 -6
  29. package/src/init-tui.ts +30 -1
  30. package/src/init.ts +11 -4
  31. package/src/normalize.ts +55 -7
  32. package/src/providers/openai-provider.ts +2 -1
  33. package/src/settings.ts +3 -2
  34. package/src/templates.ts +12 -0
  35. package/src/utils.ts +14 -0
  36. package/src/validator.ts +0 -4
  37. package/src/workflow-commands.ts +2 -1
  38. package/templates/commands/prodo-fix.md +46 -0
  39. package/templates/commands/prodo-normalize.md +116 -14
  40. package/templates/commands/prodo-prd.md +136 -12
  41. package/templates/commands/prodo-stories.md +151 -12
  42. package/templates/commands/prodo-techspec.md +165 -12
  43. package/templates/commands/prodo-validate.md +184 -23
  44. package/templates/commands/prodo-wireframe.md +186 -12
  45. package/templates/commands/prodo-workflow.md +198 -12
package/dist/agents.js CHANGED
@@ -32,7 +32,8 @@ async function loadAgentCommandSet(_cwd, agent) {
32
32
  { command: `${prefix}-wireframe`, purpose: "Generate wireframe artifact." },
33
33
  { command: `${prefix}-stories`, purpose: "Generate stories artifact." },
34
34
  { command: `${prefix}-techspec`, purpose: "Generate techspec artifact." },
35
- { command: `${prefix}-validate`, purpose: "Run validation report." }
35
+ { command: `${prefix}-validate`, purpose: "Run validation report." },
36
+ { command: `${prefix}-fix`, purpose: "Fix artifacts when validation fails." }
36
37
  ],
37
38
  artifact_shortcuts: {
38
39
  normalize: `${prefix}-normalize`,
@@ -41,7 +42,8 @@ async function loadAgentCommandSet(_cwd, agent) {
41
42
  wireframe: `${prefix}-wireframe`,
42
43
  stories: `${prefix}-stories`,
43
44
  techspec: `${prefix}-techspec`,
44
- validate: `${prefix}-validate`
45
+ validate: `${prefix}-validate`,
46
+ fix: `${prefix}-fix`
45
47
  }
46
48
  };
47
49
  }
@@ -5,5 +5,6 @@ export type GenerateOptions = {
5
5
  normalizedBriefOverride?: string;
6
6
  outPath?: string;
7
7
  agent?: string;
8
+ revisionType?: "default" | "fix";
8
9
  };
9
10
  export declare function generateArtifact(options: GenerateOptions): Promise<string>;
package/dist/artifacts.js CHANGED
@@ -20,11 +20,7 @@ const markdown_1 = require("./markdown");
20
20
  const utils_1 = require("./utils");
21
21
  const validator_1 = require("./validator");
22
22
  function defaultFilename(type) {
23
- if (type === "workflow")
24
- return `${type}-${(0, utils_1.timestampSlug)()}.md`;
25
- if (type === "wireframe")
26
- return `${type}-${(0, utils_1.timestampSlug)()}.md`;
27
- return `${type}-${(0, utils_1.timestampSlug)()}.md`;
23
+ return `${type}-${(0, utils_1.artifactFileStamp)()}.md`;
28
24
  }
29
25
  function sidecarPath(filePath) {
30
26
  const parsed = node_path_1.default.parse(filePath);
@@ -147,7 +143,166 @@ function renderWorkflowMermaidTemplate(templateContent, normalized, coverage, la
147
143
  return token;
148
144
  });
149
145
  }
150
- async function resolvePrompt(cwd, artifactType, templateContent, requiredHeadings, companionTemplate, agent) {
146
+ function normalizeAuthor(author) {
147
+ if (!author)
148
+ return undefined;
149
+ const normalized = author.trim();
150
+ return normalized.length > 0 ? normalized : undefined;
151
+ }
152
+ function replaceAuthorPlaceholders(body, author) {
153
+ const safeAuthor = normalizeAuthor(author);
154
+ if (!safeAuthor)
155
+ return body;
156
+ return body.replace(/\{\{\s*author\s*\}\}/gi, safeAuthor);
157
+ }
158
+ function todayYmd() {
159
+ const now = new Date();
160
+ const y = now.getFullYear();
161
+ const m = String(now.getMonth() + 1).padStart(2, "0");
162
+ const d = String(now.getDate()).padStart(2, "0");
163
+ return `${y}-${m}-${d}`;
164
+ }
165
+ function headingKey(value) {
166
+ return value
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9]+/g, " ")
169
+ .trim();
170
+ }
171
+ function defaultDocumentControlValues(lang, revisionType, version, author) {
172
+ const tr = lang.toLowerCase().startsWith("tr");
173
+ const safeAuthor = normalizeAuthor(author) ?? (tr ? "Prodo" : "Prodo");
174
+ const description = revisionType === "fix"
175
+ ? (tr ? "Dogrulama sonrasi duzeltme revizyonu" : "Post-validation fix revision")
176
+ : (tr ? "Ilk surum" : "Initial version");
177
+ return {
178
+ version,
179
+ date: todayYmd(),
180
+ author: safeAuthor,
181
+ description
182
+ };
183
+ }
184
+ function applyDocumentControlDefaults(body, options) {
185
+ const defaults = defaultDocumentControlValues(options.lang, options.revisionType, options.version, options.author);
186
+ let out = body
187
+ .replace(/\{\{\s*date\s*\}\}/gi, defaults.date)
188
+ .replace(/\{\{\s*description\s*\}\}/gi, defaults.description)
189
+ .replace(/\{\{\s*version\s*\}\}/gi, defaults.version);
190
+ const lines = out.split(/\r?\n/);
191
+ const headingIndex = lines.findIndex((line) => {
192
+ const match = line.match(/^\s*##+\s+(.+?)\s*$/);
193
+ if (!match)
194
+ return false;
195
+ const key = headingKey(match[1]);
196
+ return key.includes("document control") || key.includes("belge kontrol");
197
+ });
198
+ if (headingIndex === -1)
199
+ return out;
200
+ const row = `| ${defaults.version} | ${defaults.date} | ${defaults.author} | ${defaults.description} |`;
201
+ let tableSeparatorIndex = -1;
202
+ let tableDataIndex = -1;
203
+ for (let i = headingIndex + 1; i < lines.length; i += 1) {
204
+ if (/^\s*##+\s+/.test(lines[i]))
205
+ break;
206
+ if (tableSeparatorIndex === -1 && /\|/.test(lines[i]) && /-/.test(lines[i])) {
207
+ tableSeparatorIndex = i;
208
+ continue;
209
+ }
210
+ if (tableSeparatorIndex !== -1 && /^\s*\|/.test(lines[i])) {
211
+ tableDataIndex = i;
212
+ break;
213
+ }
214
+ }
215
+ if (tableDataIndex !== -1) {
216
+ lines[tableDataIndex] = row;
217
+ }
218
+ else if (tableSeparatorIndex !== -1) {
219
+ lines.splice(tableSeparatorIndex + 1, 0, row);
220
+ }
221
+ else {
222
+ lines.splice(headingIndex + 1, 0, "", "| Version | Date | Author | Description |", "|--------|------|--------|-------------|", row, "");
223
+ }
224
+ out = lines.join("\n");
225
+ return out;
226
+ }
227
+ function parseVersionToken(input) {
228
+ const match = input.match(/v?\s*(\d+)(?:\.(\d+))?/i);
229
+ if (!match)
230
+ return null;
231
+ const major = Number(match[1]);
232
+ const minor = Number(match[2] ?? "0");
233
+ if (!Number.isFinite(major) || !Number.isFinite(minor))
234
+ return null;
235
+ return { major, minor };
236
+ }
237
+ function extractDocumentControlVersion(body) {
238
+ const tableMatch = body.match(/\|\s*(v?\d+(?:\.\d+)?)\s*\|/i);
239
+ if (tableMatch?.[1])
240
+ return tableMatch[1].trim().startsWith("v") ? tableMatch[1].trim() : `v${tableMatch[1].trim()}`;
241
+ const looseMatch = body.match(/\bv?\d+\.\d+\b/i);
242
+ if (looseMatch?.[0])
243
+ return looseMatch[0].startsWith("v") ? looseMatch[0] : `v${looseMatch[0]}`;
244
+ return undefined;
245
+ }
246
+ async function resolveDocumentControlVersion(cwd, artifactType, revisionType) {
247
+ if (revisionType !== "fix")
248
+ return "v1.0";
249
+ const activePath = await (0, output_index_1.getActiveArtifactPath)(cwd, artifactType);
250
+ const fallbackPath = activePath ?? (await loadLatestArtifactPath(cwd, artifactType));
251
+ if (!fallbackPath || !(await (0, utils_1.fileExists)(fallbackPath))) {
252
+ return "v1.1";
253
+ }
254
+ try {
255
+ const previous = await loadArtifactDoc(fallbackPath);
256
+ const previousVersion = extractDocumentControlVersion(previous.body) ?? String(previous.frontmatter.version ?? "");
257
+ const parsed = parseVersionToken(previousVersion);
258
+ if (!parsed)
259
+ return "v1.1";
260
+ return `v${parsed.major}.${parsed.minor + 1}`;
261
+ }
262
+ catch {
263
+ return "v1.1";
264
+ }
265
+ }
266
+ function enforceAuthorInControlTables(body, author) {
267
+ const safeAuthor = normalizeAuthor(author);
268
+ if (!safeAuthor)
269
+ return body;
270
+ return body.replace(/(\|\s*v?[0-9.]+\s*\|\s*[^|]*\|\s*)([^|]*)(\|\s*[^|]*\|)/gi, (_match, left, _current, right) => `${left}${safeAuthor} ${right}`);
271
+ }
272
+ async function resolveUniqueOutputPath(targetDir, fileName) {
273
+ const parsed = node_path_1.default.parse(fileName);
274
+ let candidate = node_path_1.default.join(targetDir, fileName);
275
+ let index = 2;
276
+ while (await (0, utils_1.fileExists)(candidate)) {
277
+ candidate = node_path_1.default.join(targetDir, `${parsed.name}-${String(index).padStart(2, "0")}${parsed.ext}`);
278
+ index += 1;
279
+ }
280
+ return candidate;
281
+ }
282
+ function workflowFeatureTargets(normalized, coverage) {
283
+ const byId = new Map(normalized.contracts.core_features.map((item) => [item.id, item]));
284
+ const explicit = coverage.core_features
285
+ .map((id) => byId.get(id))
286
+ .filter((item) => Boolean(item));
287
+ if (explicit.length > 1)
288
+ return explicit;
289
+ if (normalized.contracts.core_features.length > 1)
290
+ return normalized.contracts.core_features.slice(0, 6);
291
+ if (explicit.length === 1)
292
+ return explicit;
293
+ return normalized.contracts.core_features.slice(0, 1);
294
+ }
295
+ function renderWorkflowMarkdownForFeature(markdown, feature, lang) {
296
+ const tr = lang.toLowerCase().startsWith("tr");
297
+ const noteHeading = tr ? "## Akis Odagi" : "## Flow Focus";
298
+ const noteLine = tr
299
+ ? `- [${feature.id}] Bu akis ${feature.text} ihtiyacina odaklanir.`
300
+ : `- [${feature.id}] This flow focuses on ${feature.text}.`;
301
+ if (markdown.includes(noteHeading))
302
+ return markdown;
303
+ return `${markdown.trim()}\n\n${noteHeading}\n${noteLine}`.trim();
304
+ }
305
+ async function resolvePrompt(cwd, artifactType, templateContent, requiredHeadings, companionTemplate, outputAuthor, agent) {
151
306
  const base = await promises_1.default.readFile((0, paths_1.promptPath)(cwd, artifactType), "utf8");
152
307
  const authority = `Template authority (STRICT):
153
308
  - Treat this template as the single output structure source.
@@ -187,12 +342,19 @@ Wireframe paired output contract (STRICT):
187
342
  - Generate companion HTML screens based on native wireframe template.
188
343
  - HTML must stay low-fidelity and structure-first.`
189
344
  : "";
345
+ const authorPolicy = outputAuthor && outputAuthor.trim().length > 0
346
+ ? `
347
+ Author policy (STRICT):
348
+ - Use this exact author name wherever author is required: ${outputAuthor.trim()}
349
+ - Do not invent random author names.`
350
+ : "";
190
351
  const withTemplate = `${base}
191
352
 
192
353
  ${authority}
193
354
  ${companionAuthority}
194
355
  ${workflowPairing}
195
- ${wireframePairing}`;
356
+ ${wireframePairing}
357
+ ${authorPolicy}`;
196
358
  if (!agent)
197
359
  return withTemplate;
198
360
  return `${withTemplate}
@@ -345,12 +507,59 @@ function splitWorkflowPair(raw) {
345
507
  }
346
508
  return { markdown, mermaid };
347
509
  }
510
+ async function writeWorkflowFlows(targetDir, baseName, normalized, coverage, lang, markdownBody, mermaidBody, mermaidTemplateContent) {
511
+ const targets = workflowFeatureTargets(normalized, coverage);
512
+ const fallbackFeature = normalized.contracts.core_features[0] ?? { id: "F1", text: "Primary flow" };
513
+ const flows = targets.length > 0 ? targets : [fallbackFeature];
514
+ const summaryBodies = [];
515
+ const renderedArtifacts = [];
516
+ let primaryMdPath = "";
517
+ for (const [index, flowFeature] of flows.entries()) {
518
+ const flowBase = flows.length === 1
519
+ ? baseName
520
+ : (index === 0
521
+ ? baseName
522
+ : `${baseName}-${index + 1}-${toSlug(extractTurkishTitle(flowFeature.text))}`);
523
+ const mdPath = node_path_1.default.join(targetDir, `${flowBase}.md`);
524
+ const mmdPath = node_path_1.default.join(targetDir, `${flowBase}.mmd`);
525
+ const featureCoverage = {
526
+ ...coverage,
527
+ core_features: [flowFeature.id]
528
+ };
529
+ const renderedMarkdown = renderWorkflowMarkdownForFeature(markdownBody, flowFeature, lang);
530
+ const renderedMermaid = (mermaidTemplateContent && mermaidTemplateContent.trim().length > 0)
531
+ ? renderWorkflowMermaidTemplate(mermaidTemplateContent, normalized, featureCoverage, lang).trim()
532
+ : (mermaidBody ?? "").trim();
533
+ if (!/(^|\n)\s*(flowchart|graph)\s+/i.test(renderedMermaid)) {
534
+ throw new errors_1.UserError("Workflow Mermaid output is invalid.");
535
+ }
536
+ enforceLanguage(renderedMarkdown, lang, "workflow");
537
+ enforceLanguage(renderedMermaid, lang, "workflow");
538
+ await promises_1.default.writeFile(mdPath, `${renderedMarkdown}\n`, "utf8");
539
+ await promises_1.default.writeFile(mmdPath, `${renderedMermaid}\n`, "utf8");
540
+ if (!primaryMdPath)
541
+ primaryMdPath = mdPath;
542
+ summaryBodies.push(renderedMarkdown);
543
+ renderedArtifacts.push({ mdPath, body: renderedMarkdown });
544
+ }
545
+ return {
546
+ primaryPath: primaryMdPath,
547
+ summaryBody: summaryBodies.join("\n\n"),
548
+ rendered: renderedArtifacts
549
+ };
550
+ }
348
551
  async function writeWireframeScreens(targetDir, baseName, normalized, coverage, lang, headings, htmlTemplateContent) {
349
552
  const tr = lang.toLowerCase().startsWith("tr");
350
- const screenContracts = normalized.contracts.core_features
553
+ const explicitScreens = normalized.contracts.core_features
351
554
  .filter((item) => coverage.core_features.includes(item.id))
352
555
  .slice(0, 6);
353
- const screens = screenContracts.length > 0 ? screenContracts : normalized.contracts.core_features.slice(0, 3);
556
+ const screens = explicitScreens.length > 1
557
+ ? explicitScreens
558
+ : (normalized.contracts.core_features.length > 1
559
+ ? normalized.contracts.core_features.slice(0, 6)
560
+ : (explicitScreens.length === 1
561
+ ? explicitScreens
562
+ : normalized.contracts.core_features.slice(0, 1)));
354
563
  const summaryBodies = [];
355
564
  let primaryMdPath = "";
356
565
  for (const [index, screen] of screens.entries()) {
@@ -486,9 +695,11 @@ async function writeWireframeScreens(targetDir, baseName, normalized, coverage,
486
695
  }
487
696
  async function generateArtifact(options) {
488
697
  const { cwd, artifactType, outPath, agent } = options;
698
+ const revisionType = options.revisionType ?? "default";
489
699
  const def = await (0, artifact_registry_1.getArtifactDefinition)(cwd, artifactType);
490
700
  const normalizedPath = options.normalizedBriefOverride ?? (0, paths_1.normalizedBriefPath)(cwd);
491
701
  await ensurePipelinePrereqs(cwd, normalizedPath);
702
+ const documentControlVersion = await resolveDocumentControlVersion(cwd, artifactType, revisionType);
492
703
  const settings = await (0, settings_1.readSettings)(cwd);
493
704
  const normalizedBriefRaw = await (0, utils_1.readJsonFile)(normalizedPath);
494
705
  const normalizedBrief = (0, normalized_brief_1.parseNormalizedBriefOrThrow)(normalizedBriefRaw);
@@ -510,7 +721,7 @@ async function generateArtifact(options) {
510
721
  const computedHeadings = templateHeadings.length > 0
511
722
  ? templateHeadings
512
723
  : (def.required_headings.length > 0 ? def.required_headings : (0, constants_1.defaultRequiredHeadings)(artifactType));
513
- const prompt = await resolvePrompt(cwd, artifactType, template?.content ?? "", computedHeadings, companionTemplate, agent);
724
+ const prompt = await resolvePrompt(cwd, artifactType, template?.content ?? "", computedHeadings, companionTemplate, settings.author, agent);
514
725
  const provider = (0, providers_1.createProvider)();
515
726
  const upstreamArtifacts = await buildUpstreamArtifacts(cwd, artifactType, def.upstream);
516
727
  const schemaHint = {
@@ -526,14 +737,15 @@ async function generateArtifact(options) {
526
737
  templatePath: template?.path ?? "",
527
738
  companionTemplateContent: companionTemplate?.content ?? "",
528
739
  companionTemplatePath: companionTemplate?.path ?? "",
529
- outputLanguage: settings.lang
740
+ outputLanguage: settings.lang,
741
+ outputAuthor: settings.author
530
742
  }, schemaHint);
531
- let generatedBody = generated.body.trim();
743
+ let generatedBody = enforceAuthorInControlTables(replaceAuthorPlaceholders(generated.body.trim(), settings.author), settings.author);
532
744
  let workflowMermaidBody = null;
533
745
  if (artifactType === "workflow") {
534
746
  const paired = splitWorkflowPair(generatedBody);
535
- generatedBody = paired.markdown;
536
- workflowMermaidBody = paired.mermaid;
747
+ generatedBody = enforceAuthorInControlTables(replaceAuthorPlaceholders(paired.markdown, settings.author), settings.author);
748
+ workflowMermaidBody = replaceAuthorPlaceholders(paired.mermaid, settings.author);
537
749
  }
538
750
  let contractCoverage = extractCoverageFromBody(generatedBody);
539
751
  if (artifactType === "workflow") {
@@ -552,8 +764,15 @@ async function generateArtifact(options) {
552
764
  };
553
765
  }
554
766
  }
767
+ generatedBody = applyDocumentControlDefaults(generatedBody, {
768
+ lang: settings.lang,
769
+ revisionType,
770
+ version: documentControlVersion,
771
+ author: settings.author
772
+ });
555
773
  if (artifactType === "workflow" && companionTemplate?.content) {
556
774
  workflowMermaidBody = renderWorkflowMermaidTemplate(companionTemplate.content, normalizedBrief, contractCoverage, settings.lang).trim();
775
+ workflowMermaidBody = replaceAuthorPlaceholders(workflowMermaidBody, settings.author);
557
776
  }
558
777
  enforceLanguage(generatedBody, settings.lang, artifactType);
559
778
  if (artifactType === "workflow" && workflowMermaidBody) {
@@ -574,9 +793,13 @@ async function generateArtifact(options) {
574
793
  status: constants_1.DEFAULT_STATUS,
575
794
  upstream_artifacts: upstreamArtifacts.map((item) => item.file),
576
795
  contract_coverage: contractCoverage,
577
- language: settings.lang
796
+ language: settings.lang,
797
+ ...(normalizeAuthor(settings.author) ? { author: normalizeAuthor(settings.author) } : {})
578
798
  };
579
799
  const mergedFrontmatter = { ...frontmatter, ...(generated.frontmatter ?? {}) };
800
+ if (normalizeAuthor(settings.author)) {
801
+ mergedFrontmatter.author = normalizeAuthor(settings.author);
802
+ }
580
803
  let doc = {
581
804
  frontmatter: mergedFrontmatter,
582
805
  body: generatedBody
@@ -588,29 +811,40 @@ async function generateArtifact(options) {
588
811
  throw new errors_1.UserError(`Artifact failed schema checks:\n${details}`);
589
812
  }
590
813
  const targetDir = (0, paths_1.outputDirPath)(cwd, artifactType, def.output_dir);
591
- const finalPath = outPath ? node_path_1.default.resolve(cwd, outPath) : node_path_1.default.join(targetDir, defaultFilename(artifactType));
814
+ const finalPath = outPath
815
+ ? node_path_1.default.resolve(cwd, outPath)
816
+ : await resolveUniqueOutputPath(targetDir, defaultFilename(artifactType));
592
817
  if (!(0, utils_1.isPathInside)(node_path_1.default.join(cwd, "product-docs"), finalPath)) {
593
818
  throw new errors_1.UserError("Artifact output must be inside `product-docs/`.");
594
819
  }
595
820
  await promises_1.default.mkdir(node_path_1.default.dirname(finalPath), { recursive: true });
596
821
  if (artifactType === "workflow") {
597
822
  const basePath = node_path_1.default.join(node_path_1.default.dirname(finalPath), node_path_1.default.parse(finalPath).name);
598
- const mdPath = `${basePath}.md`;
599
- const mmdPath = `${basePath}.mmd`;
600
- await promises_1.default.writeFile(mdPath, gray_matter_1.default.stringify(doc.body, doc.frontmatter), "utf8");
601
- await promises_1.default.writeFile(mmdPath, `${(workflowMermaidBody ?? "").trim()}\n`, "utf8");
602
- await writeSidecar(mdPath, doc);
603
- const derivedContext = {
604
- artifact_type: artifactType,
605
- artifact_file: mdPath,
606
- generated_at: new Date().toISOString(),
607
- contract_coverage: contractCoverage,
608
- ...deriveStructuredContext(artifactType, doc.body, schemaHint.requiredHeadings)
609
- };
823
+ const workflow = await writeWorkflowFlows(node_path_1.default.dirname(basePath), node_path_1.default.parse(basePath).name, normalizedBrief, contractCoverage, settings.lang, doc.body, workflowMermaidBody, companionTemplate?.content ?? null);
610
824
  await promises_1.default.mkdir((0, paths_1.outputContextDirPath)(cwd), { recursive: true });
611
- await promises_1.default.writeFile(contextFilePath(cwd, mdPath), `${JSON.stringify(derivedContext, null, 2)}\n`, "utf8");
612
- await (0, output_index_1.setActiveArtifact)(cwd, artifactType, mdPath);
613
- return mdPath;
825
+ for (const rendered of workflow.rendered) {
826
+ const renderedDoc = {
827
+ frontmatter: doc.frontmatter,
828
+ body: rendered.body
829
+ };
830
+ await promises_1.default.writeFile(rendered.mdPath, gray_matter_1.default.stringify(renderedDoc.body, renderedDoc.frontmatter), "utf8");
831
+ await writeSidecar(rendered.mdPath, renderedDoc);
832
+ const renderedContext = {
833
+ artifact_type: artifactType,
834
+ artifact_file: rendered.mdPath,
835
+ generated_at: new Date().toISOString(),
836
+ contract_coverage: contractCoverage,
837
+ ...deriveStructuredContext(artifactType, renderedDoc.body, schemaHint.requiredHeadings)
838
+ };
839
+ await promises_1.default.writeFile(contextFilePath(cwd, rendered.mdPath), `${JSON.stringify(renderedContext, null, 2)}\n`, "utf8");
840
+ }
841
+ const primaryRendered = workflow.rendered.find((item) => item.mdPath === workflow.primaryPath) ?? workflow.rendered[0];
842
+ doc = {
843
+ frontmatter: doc.frontmatter,
844
+ body: primaryRendered?.body ?? doc.body
845
+ };
846
+ await (0, output_index_1.setActiveArtifact)(cwd, artifactType, workflow.primaryPath);
847
+ return workflow.primaryPath;
614
848
  }
615
849
  else if (artifactType === "wireframe") {
616
850
  const base = node_path_1.default.parse(finalPath).name;
package/dist/cli.js CHANGED
@@ -29,6 +29,8 @@ function mapForcedCommand(forcedCommand) {
29
29
  return "validate";
30
30
  if (forcedCommand === "prodo-normalize")
31
31
  return "normalize";
32
+ if (forcedCommand === "prodo-fix")
33
+ return "fix";
32
34
  if (forcedCommand === "prodo-prd")
33
35
  return "prd";
34
36
  if (forcedCommand === "prodo-workflow")
@@ -49,7 +51,8 @@ async function runArtifactCommand(type, opts, cwd, log, options) {
49
51
  cwd,
50
52
  normalizedBriefOverride: opts.from,
51
53
  outPath: opts.out,
52
- agent
54
+ agent,
55
+ revisionType: opts.revisionType
53
56
  });
54
57
  const agentMsg = agent ? ` [agent=${agent}]` : "";
55
58
  log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
@@ -100,13 +103,15 @@ async function runCli(options = {}) {
100
103
  .command("init [target]")
101
104
  .option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
102
105
  .option("--lang <code>", "document language (e.g. en, tr)")
106
+ .option("--author <name>", "document author name")
103
107
  .option("--preset <name>", "preset to install during initialization")
104
108
  .action(async (target, opts) => {
105
109
  const projectRoot = node_path_1.default.resolve(cwd, target ?? ".");
106
110
  const selected = await (0, init_tui_1.gatherInitSelections)({
107
111
  projectRoot,
108
112
  aiInput: opts.ai,
109
- langInput: opts.lang
113
+ langInput: opts.lang,
114
+ authorInput: opts.author
110
115
  });
111
116
  const selectedAi = selected.ai;
112
117
  if (selected.interactive) {
@@ -116,6 +121,7 @@ async function runCli(options = {}) {
116
121
  const result = await (0, init_1.runInit)(projectRoot, {
117
122
  ai: selectedAi,
118
123
  lang: selected.lang,
124
+ author: selected.author,
119
125
  preset: opts.preset,
120
126
  script: selected.script
121
127
  });
@@ -124,13 +130,15 @@ async function runCli(options = {}) {
124
130
  projectRoot,
125
131
  settingsPath: result.settingsPath,
126
132
  ai: selectedAi,
127
- lang: selected.lang
133
+ lang: selected.lang,
134
+ author: selected.author
128
135
  });
129
136
  return;
130
137
  }
131
138
  const result = await (0, init_1.runInit)(projectRoot, {
132
139
  ai: selectedAi,
133
140
  lang: selected.lang,
141
+ author: selected.author,
134
142
  preset: opts.preset,
135
143
  script: selected.script
136
144
  });
@@ -144,6 +152,7 @@ async function runCli(options = {}) {
144
152
  out("No agent selected. Use `prodo generate` for end-to-end generation.");
145
153
  }
146
154
  out(`Settings file: ${result.settingsPath}`);
155
+ out(`Author: ${selected.author}`);
147
156
  out("Next: edit brief.md.");
148
157
  });
149
158
  program
@@ -178,6 +187,51 @@ async function runCli(options = {}) {
178
187
  await (0, hook_executor_1.runHookPhase)(cwd, "after_validate", out);
179
188
  });
180
189
  });
190
+ program
191
+ .command("fix", { hidden: true })
192
+ .description("Advanced: auto-regenerate affected artifacts from validation findings")
193
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
194
+ .option("--strict", "treat validation warnings as errors")
195
+ .option("--report <path>", "validation report output path")
196
+ .action(async (opts) => {
197
+ if (opts.agent)
198
+ (0, agents_1.resolveAgent)(opts.agent);
199
+ await withBriefReadOnlyGuard(cwd, async () => {
200
+ await (0, hook_executor_1.runHookPhase)(cwd, "before_validate", out);
201
+ const initial = await (0, validate_1.runValidate)(cwd, {
202
+ strict: Boolean(opts.strict),
203
+ report: opts.report
204
+ });
205
+ out(`Validation report written to: ${initial.reportPath}`);
206
+ await (0, hook_executor_1.runHookPhase)(cwd, "after_validate", out);
207
+ if (initial.pass) {
208
+ out("No blocking issues found. Nothing to fix.");
209
+ return;
210
+ }
211
+ const targets = await resolveFixTargets(cwd, artifactTypes, initial.issues);
212
+ out(`Validation failed. Regenerating impacted artifacts: ${targets.join(", ")}`);
213
+ await (0, hook_executor_1.runHookPhase)(cwd, "before_normalize", out);
214
+ const normalizedPath = await (0, normalize_1.runNormalize)({ cwd });
215
+ out(`Normalized brief refreshed: ${normalizedPath}`);
216
+ await (0, hook_executor_1.runHookPhase)(cwd, "after_normalize", out);
217
+ for (const type of targets) {
218
+ await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent, revisionType: "fix" }, cwd, out, {
219
+ suggestValidate: false
220
+ });
221
+ }
222
+ await (0, hook_executor_1.runHookPhase)(cwd, "before_validate", out);
223
+ const final = await (0, validate_1.runValidate)(cwd, {
224
+ strict: Boolean(opts.strict),
225
+ report: opts.report
226
+ });
227
+ out(`Validation report written to: ${final.reportPath}`);
228
+ if (!final.pass) {
229
+ throw new errors_1.UserError("Fix completed but validation is still failing. Review report and retry.");
230
+ }
231
+ out("Fix pipeline completed. Validation passed.");
232
+ await (0, hook_executor_1.runHookPhase)(cwd, "after_validate", out);
233
+ });
234
+ });
181
235
  program
182
236
  .command("normalize", { hidden: true })
183
237
  .description("Advanced: normalize brief without full pipeline")
@@ -276,6 +330,9 @@ async function runCli(options = {}) {
276
330
  else if (forced === "validate") {
277
331
  await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
278
332
  }
333
+ else if (forced === "fix") {
334
+ await program.parseAsync(["node", "prodo", "fix", ...argv.slice(2)]);
335
+ }
279
336
  else {
280
337
  await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
281
338
  }
@@ -300,3 +357,23 @@ if (require.main === module) {
300
357
  process.exitCode = code;
301
358
  });
302
359
  }
360
+ async function resolveFixTargets(cwd, artifactTypes, issues) {
361
+ const direct = new Set(issues
362
+ .map((issue) => issue.artifactType)
363
+ .filter((artifactType) => typeof artifactType === "string" && artifactTypes.includes(artifactType)));
364
+ if (direct.size === 0)
365
+ return artifactTypes;
366
+ const defs = await (0, artifact_registry_1.listArtifactDefinitions)(cwd);
367
+ let changed = true;
368
+ while (changed) {
369
+ changed = false;
370
+ for (const def of defs) {
371
+ const needsRefresh = def.upstream.some((upstream) => direct.has(upstream));
372
+ if (needsRefresh && !direct.has(def.name)) {
373
+ direct.add(def.name);
374
+ changed = true;
375
+ }
376
+ }
377
+ }
378
+ return artifactTypes.filter((artifactType) => direct.has(artifactType));
379
+ }
@@ -3,12 +3,14 @@ export type InitSelections = {
3
3
  ai?: SupportedAi;
4
4
  script: "sh" | "ps";
5
5
  lang: "tr" | "en";
6
+ author: string;
6
7
  interactive: boolean;
7
8
  };
8
9
  type GatherInitUiOptions = {
9
10
  projectRoot: string;
10
11
  aiInput?: string;
11
12
  langInput?: string;
13
+ authorInput?: string;
12
14
  };
13
15
  export declare function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections>;
14
16
  export declare function finishInitInteractive(summary: {
@@ -16,5 +18,6 @@ export declare function finishInitInteractive(summary: {
16
18
  settingsPath: string;
17
19
  ai?: SupportedAi;
18
20
  lang: "tr" | "en";
21
+ author: string;
19
22
  }): Promise<void>;
20
23
  export {};
package/dist/init-tui.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.gatherInitSelections = gatherInitSelections;
7
7
  exports.finishInitInteractive = finishInitInteractive;
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
+ const node_os_1 = __importDefault(require("node:os"));
9
10
  const agent_command_installer_1 = require("./agent-command-installer");
10
11
  const errors_1 = require("./errors");
11
12
  const utils_1 = require("./utils");
@@ -75,16 +76,32 @@ function modelChoiceFromAi(ai) {
75
76
  return "gemini";
76
77
  return "auto-detect";
77
78
  }
79
+ function defaultAuthorName(authorInput) {
80
+ const explicit = (authorInput ?? "").trim();
81
+ if (explicit.length > 0)
82
+ return explicit;
83
+ try {
84
+ const username = node_os_1.default.userInfo().username.trim();
85
+ if (username.length > 0)
86
+ return username;
87
+ }
88
+ catch {
89
+ // ignore lookup errors and continue with fallback
90
+ }
91
+ return "Product Author";
92
+ }
78
93
  async function gatherInitSelections(options) {
79
94
  const clack = await loadClack();
80
95
  const defaultLang = normalizeLang(options.langInput);
81
96
  const fallbackScript = process.platform === "win32" ? "ps" : "sh";
82
97
  const parsedAi = (0, agent_command_installer_1.resolveAi)(options.aiInput);
98
+ const defaultAuthor = defaultAuthorName(options.authorInput);
83
99
  if (!isInteractiveTerminal()) {
84
100
  return {
85
101
  ai: parsedAi,
86
102
  script: fallbackScript,
87
103
  lang: defaultLang,
104
+ author: defaultAuthor,
88
105
  interactive: false
89
106
  };
90
107
  }
@@ -129,6 +146,15 @@ async function gatherInitSelections(options) {
129
146
  clack.cancel("Initialization cancelled.");
130
147
  throw new errors_1.UserError("Initialization cancelled.");
131
148
  }
149
+ const author = await clack.text({
150
+ message: "Author name",
151
+ placeholder: "Shahmarasy",
152
+ defaultValue: defaultAuthor
153
+ });
154
+ if (clack.isCancel(author)) {
155
+ clack.cancel("Initialization cancelled.");
156
+ throw new errors_1.UserError("Initialization cancelled.");
157
+ }
132
158
  let selectedAi;
133
159
  if (model === "codex")
134
160
  selectedAi = "codex";
@@ -140,10 +166,11 @@ async function gatherInitSelections(options) {
140
166
  ai: selectedAi,
141
167
  script: fallbackScript,
142
168
  lang,
169
+ author: String(author).trim() || defaultAuthor,
143
170
  interactive: true
144
171
  };
145
172
  }
146
173
  function finishInitInteractive(summary) {
147
174
  const aiText = summary.ai ?? "none";
148
- return loadClack().then((clack) => clack.outro(`Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`));
175
+ return loadClack().then((clack) => clack.outro(`Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nAuthor: ${summary.author}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`));
149
176
  }
package/dist/init.d.ts CHANGED
@@ -2,6 +2,7 @@ import { type SupportedAi } from "./agent-command-installer";
2
2
  export declare function runInit(cwd: string, options?: {
3
3
  ai?: SupportedAi;
4
4
  lang?: string;
5
+ author?: string;
5
6
  preset?: string;
6
7
  script?: "sh" | "ps";
7
8
  }): Promise<{