@shahmarasy/prodo 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents.js +4 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +265 -31
- package/dist/cli.js +80 -3
- package/dist/init-tui.d.ts +3 -0
- package/dist/init-tui.js +28 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +9 -3
- package/dist/normalize.js +55 -7
- package/dist/providers/openai-provider.js +2 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +11 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/validator.js +0 -4
- package/dist/workflow-commands.js +2 -1
- package/package.json +1 -1
- package/presets/fintech/preset.json +48 -1
- package/presets/fintech/prompts/prd.md +99 -2
- package/presets/marketplace/preset.json +51 -1
- package/presets/marketplace/prompts/prd.md +140 -2
- package/presets/saas/preset.json +53 -1
- package/presets/saas/prompts/prd.md +150 -2
- package/src/agents.ts +4 -2
- package/src/artifacts.ts +323 -28
- package/src/cli.ts +97 -6
- package/src/init-tui.ts +30 -1
- package/src/init.ts +11 -4
- package/src/normalize.ts +55 -7
- package/src/providers/openai-provider.ts +2 -1
- package/src/settings.ts +3 -2
- package/src/templates.ts +12 -0
- package/src/utils.ts +14 -0
- package/src/validator.ts +0 -4
- package/src/workflow-commands.ts +2 -1
- package/templates/commands/prodo-fix.md +46 -0
- package/templates/commands/prodo-normalize.md +116 -14
- package/templates/commands/prodo-prd.md +136 -12
- package/templates/commands/prodo-stories.md +151 -12
- package/templates/commands/prodo-techspec.md +165 -12
- package/templates/commands/prodo-validate.md +184 -23
- package/templates/commands/prodo-wireframe.md +186 -12
- package/templates/commands/prodo-workflow.md +198 -12
package/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
|
}
|
package/dist/artifacts.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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
|
+
}
|
package/dist/init-tui.d.ts
CHANGED
|
@@ -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
|
}
|