@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.
- 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 +2 -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 +2 -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 +118 -23
- package/templates/commands/prodo-prd.md +138 -17
- package/templates/commands/prodo-stories.md +153 -17
- package/templates/commands/prodo-techspec.md +167 -17
- package/templates/commands/prodo-validate.md +184 -26
- package/templates/commands/prodo-wireframe.md +188 -17
- package/templates/commands/prodo-workflow.md +200 -17
|
@@ -1,3 +1,151 @@
|
|
|
1
|
-
Preset
|
|
1
|
+
# SaaS Preset - PRD Generation Context
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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(
|