@shahmarasy/prodo 0.1.3 → 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 +2 -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 +2 -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 +118 -23
  40. package/templates/commands/prodo-prd.md +138 -17
  41. package/templates/commands/prodo-stories.md +153 -17
  42. package/templates/commands/prodo-techspec.md +167 -17
  43. package/templates/commands/prodo-validate.md +184 -26
  44. package/templates/commands/prodo-wireframe.md +188 -17
  45. package/templates/commands/prodo-workflow.md +200 -17
@@ -1,3 +1,151 @@
1
- Preset: saas
1
+ # SaaS Preset - PRD Generation Context
2
2
 
3
- Use domain-specific terminology and constraints for saas products.
3
+ ## Preset Overview
4
+ **Domain**: Software as a Service (SaaS)
5
+ **Focus**: Subscription economics, customer success, enterprise integration, scalability, recurring revenue
6
+ **Target Models**: Enterprise Software, Business Tools, Vertical SaaS, Workflow Automation, Analytics
7
+
8
+ ## Critical Requirements for SaaS Products
9
+
10
+ ### 1. Subscription Business Model
11
+ - **Pricing Tiers**: Free/Freemium, Starter, Professional, Enterprise
12
+ - **Billing Cycle**: Monthly, annual, or usage-based
13
+ - **Per-Seat vs. Fixed**: User-based vs. flat-rate pricing
14
+ - **Feature Gating**: Which features per tier
15
+ - **Contract Terms**: Auto-renewal, cancellation terms, trial periods
16
+
17
+ ### 2. Customer Onboarding & Adoption
18
+ - **Self-Serve Onboarding**: Guided setup, templates, presets
19
+ - **Product-Led Growth**: Feature discoverability, in-app education
20
+ - **User Training**: Tutorials, help center, webinars, documentation
21
+ - **Activation**: Time to first value, key engagement metrics
22
+ - **Expansion Path**: Upsell opportunities, feature education
23
+
24
+ ### 3. Enterprise Integration
25
+ - **SSO/SAML**: Single sign-on for corporate users
26
+ - **APIs**: REST/GraphQL APIs for custom integrations
27
+ - **Webhooks**: Real-time event notifications
28
+ - **Data Export**: Export capabilities, compliance
29
+ - **Native Integrations**: Slack, Salesforce, HubSpot, Jira, etc.
30
+
31
+ ### 4. Scalability & Multi-Tenancy
32
+ - **Tenant Isolation**: Data separation and security
33
+ - **Performance**: Sub-second response times
34
+ - **Availability**: 99.9% or 99.99% uptime SLA
35
+ - **Disaster Recovery**: Backup, replication, failover
36
+ - **Geographic Distribution**: Multi-region support
37
+
38
+ ### 5. Customer Success & Retention
39
+ - **Onboarding Support**: Dedicated CSM for enterprise accounts
40
+ - **Health Scoring**: Identify at-risk customers proactively
41
+ - **Usage Analytics**: Monitor feature adoption and user behavior
42
+ - **Support Tiers**: Community, email, chat, phone, 24/7
43
+ - **Renewal Playbook**: Steps to improve retention
44
+
45
+ ### 6. Compliance & Data Governance
46
+ - **SOC 2**: Security, availability, confidentiality standards
47
+ - **HIPAA**: For healthcare SaaS
48
+ - **GDPR/CCPA**: Data privacy and right to deletion
49
+ - **Data Residency**: Geographic data storage requirements
50
+ - **Audit Trails**: Logging for compliance audits
51
+
52
+ ### 7. User Personas (SaaS Specific)
53
+
54
+ #### End User
55
+ - Goals: Solve their job efficiently, learn easily
56
+ - Pain Points: Complex workflows, context-switching, training
57
+ - Concerns: Time investment, productivity gain
58
+
59
+ #### Admin/IT
60
+ - Goals: Secure deployment, central management, compliance
61
+ - Pain Points: User management overhead, security config
62
+ - Concerns: Data security, compliance, integration complexity
63
+
64
+ #### Finance Lead
65
+ - Goals: ROI justification, cost optimization, budgeting
66
+ - Pain Points: License management, usage visibility
67
+ - Concerns: TCO, renewal negotiations, cost per user
68
+
69
+ #### Customer Success Manager
70
+ - Goals: Customer adoption, expansion, retention
71
+ - Pain Points: Health scoring accuracy, expansion opportunities
72
+ - Concerns: Churn prevention, engagement metrics, renewal success
73
+
74
+ #### Developer (if applicable)
75
+ - Goals: Easy integration, extensibility, good documentation
76
+ - Pain Points: API limitations, rate limits, debugging
77
+ - Concerns: Developer experience, SDK quality, support
78
+
79
+ ### 8. Success Metrics (SaaS Specific)
80
+ - **Monthly Recurring Revenue (MRR)**: Predictable revenue base
81
+ - **Annual Recurring Revenue (ARR)**: Annualized MRR
82
+ - **Customer Acquisition Cost (CAC)**: Cost to acquire customer
83
+ - **Lifetime Value (LTV)**: Total revenue per customer
84
+ - **LTV:CAC Ratio**: Should be 3:1 or higher (industry standard)
85
+ - **Churn Rate**: Monthly % of customers leaving
86
+ - **Net Revenue Retention (NRR)**: Retention + expansion
87
+ - **Customer Health Score**: Predictive churn indicator
88
+ - **Net Promoter Score (NPS)**: Customer satisfaction
89
+ - **Time to Value (TTV)**: Days to first value realization
90
+
91
+ ### 9. Key Constraints
92
+ - **Uptime SLA**: 99.9% minimum (often 99.99% expected)
93
+ - **Data Residency**: Specific geographic regions required
94
+ - **Integration Requirements**: Must support popular tools
95
+ - **Pricing Sensitivity**: Competitive pricing pressure
96
+ - **Contract Terms**: Annual vs. monthly, lock-in periods
97
+ - **Support Costs**: Scale with customer base and tier
98
+ - **Regulatory Requirements**: Compliance obligations
99
+
100
+ ## PRD Generation Guidelines
101
+
102
+ ### Subscription-First Thinking
103
+ - Every feature → How does it impact retention?
104
+ - Every feature → Expansion opportunity or anti-churn?
105
+ - Every feature → Which tier includes it?
106
+ - Pricing change → Impact on LTV and churn?
107
+
108
+ ### Customer Success Playbook
109
+ - How do we onboard customers?
110
+ - How do we measure adoption?
111
+ - How do we identify expansion opportunities?
112
+ - How do we prevent churn?
113
+ - What's the health scoring model?
114
+
115
+ ### Enterprise Requirements
116
+ - What integrations are table-stakes?
117
+ - What compliance is required?
118
+ - What's the uptime SLA?
119
+ - What's the support model?
120
+ - What's the pricing model for enterprise?
121
+
122
+ ### API-First Design
123
+ - What's exposed via API?
124
+ - What data can be exported?
125
+ - What webhooks/events exist?
126
+ - Rate limiting strategy?
127
+ - Documentation quality?
128
+
129
+ ### Scalability Considerations
130
+ - How does the system scale to 10k users?
131
+ - How does the system scale to 100k users?
132
+ - What's the performance baseline?
133
+ - What monitoring is needed?
134
+ - What's the disaster recovery plan?
135
+
136
+ ## Validation Checklist
137
+ Before finalizing PRD, verify:
138
+ - ✅ Pricing model clearly defined (tiers, billing, trial)
139
+ - ✅ Onboarding flow documented (self-serve path)
140
+ - ✅ Key integrations listed
141
+ - ✅ SLA/uptime committed (99.9% minimum)
142
+ - ✅ Compliance requirements identified
143
+ - ✅ Customer success strategy outlined
144
+ - ✅ Health scoring model conceptualized
145
+ - ✅ Support tier structure defined
146
+ - ✅ Churn prevention strategy exists
147
+ - ✅ Expansion/upsell opportunities identified
148
+ - ✅ API strategy documented
149
+ - ✅ Multi-tenancy approach clear
150
+ - ✅ Disaster recovery plan outlined
151
+ - ✅ Geographic expansion strategy noted
package/src/agents.ts CHANGED
@@ -41,7 +41,8 @@ export async function loadAgentCommandSet(_cwd: string, agent: AgentId): Promise
41
41
  { command: `${prefix}-wireframe`, purpose: "Generate wireframe artifact." },
42
42
  { command: `${prefix}-stories`, purpose: "Generate stories artifact." },
43
43
  { command: `${prefix}-techspec`, purpose: "Generate techspec artifact." },
44
- { command: `${prefix}-validate`, purpose: "Run validation report." }
44
+ { command: `${prefix}-validate`, purpose: "Run validation report." },
45
+ { command: `${prefix}-fix`, purpose: "Fix artifacts when validation fails." }
45
46
  ],
46
47
  artifact_shortcuts: {
47
48
  normalize: `${prefix}-normalize`,
@@ -50,7 +51,8 @@ export async function loadAgentCommandSet(_cwd: string, agent: AgentId): Promise
50
51
  wireframe: `${prefix}-wireframe`,
51
52
  stories: `${prefix}-stories`,
52
53
  techspec: `${prefix}-techspec`,
53
- validate: `${prefix}-validate`
54
+ validate: `${prefix}-validate`,
55
+ fix: `${prefix}-fix`
54
56
  }
55
57
  };
56
58
  }
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
- if (type === "workflow") return `${type}-${timestampSlug()}.md`;
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 screenContracts = normalized.contracts.core_features
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 = screenContracts.length > 0 ? screenContracts : normalized.contracts.core_features.slice(0, 3);
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 = generated.body.trim();
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 = paired.markdown;
641
- workflowMermaidBody = paired.mermaid;
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 ? path.resolve(cwd, outPath) : path.join(targetDir, defaultFilename(artifactType));
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 mdPath = `${basePath}.md`;
719
- const mmdPath = `${basePath}.mmd`;
720
- await fs.writeFile(mdPath, matter.stringify(doc.body, doc.frontmatter), "utf8");
721
- await fs.writeFile(mmdPath, `${(workflowMermaidBody ?? "").trim()}\n`, "utf8");
722
- await writeSidecar(mdPath, doc);
723
- const derivedContext = {
724
- artifact_type: artifactType,
725
- artifact_file: mdPath,
726
- generated_at: new Date().toISOString(),
727
- contract_coverage: contractCoverage,
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
- await fs.writeFile(contextFilePath(cwd, mdPath), `${JSON.stringify(derivedContext, null, 2)}\n`, "utf8");
732
- await setActiveArtifact(cwd, artifactType, mdPath);
733
- return mdPath;
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(