@shahmarasy/prodo 0.1.0
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/LICENSE +21 -0
- package/README.md +157 -0
- package/bin/prodo.cjs +6 -0
- package/dist/agent-command-installer.d.ts +4 -0
- package/dist/agent-command-installer.js +158 -0
- package/dist/agents.d.ts +15 -0
- package/dist/agents.js +47 -0
- package/dist/artifact-registry.d.ts +11 -0
- package/dist/artifact-registry.js +49 -0
- package/dist/artifacts.d.ts +9 -0
- package/dist/artifacts.js +514 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +305 -0
- package/dist/consistency.d.ts +8 -0
- package/dist/consistency.js +268 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +64 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +123 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +10 -0
- package/dist/hook-executor.d.ts +1 -0
- package/dist/hook-executor.js +175 -0
- package/dist/init-tui.d.ts +21 -0
- package/dist/init-tui.js +161 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +307 -0
- package/dist/markdown.d.ts +11 -0
- package/dist/markdown.js +66 -0
- package/dist/normalize.d.ts +7 -0
- package/dist/normalize.js +73 -0
- package/dist/normalized-brief.d.ts +39 -0
- package/dist/normalized-brief.js +170 -0
- package/dist/output-index.d.ts +13 -0
- package/dist/output-index.js +55 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +76 -0
- package/dist/preset-loader.d.ts +4 -0
- package/dist/preset-loader.js +210 -0
- package/dist/project-config.d.ts +14 -0
- package/dist/project-config.js +69 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/mock-provider.d.ts +7 -0
- package/dist/providers/mock-provider.js +168 -0
- package/dist/providers/openai-provider.d.ts +11 -0
- package/dist/providers/openai-provider.js +69 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.js +115 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +34 -0
- package/dist/template-resolver.d.ts +11 -0
- package/dist/template-resolver.js +28 -0
- package/dist/templates.d.ts +33 -0
- package/dist/templates.js +428 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +53 -0
- package/dist/validate.d.ts +9 -0
- package/dist/validate.js +226 -0
- package/dist/validator.d.ts +5 -0
- package/dist/validator.js +80 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +30 -0
- package/dist/workflow-commands.d.ts +7 -0
- package/dist/workflow-commands.js +28 -0
- package/package.json +45 -0
- package/presets/fintech/preset.json +1 -0
- package/presets/fintech/prompts/prd.md +3 -0
- package/presets/marketplace/preset.json +1 -0
- package/presets/marketplace/prompts/prd.md +3 -0
- package/presets/saas/preset.json +1 -0
- package/presets/saas/prompts/prd.md +3 -0
- package/src/agent-command-installer.ts +174 -0
- package/src/agents.ts +56 -0
- package/src/artifact-registry.ts +69 -0
- package/src/artifacts.ts +606 -0
- package/src/cli.ts +322 -0
- package/src/consistency.ts +303 -0
- package/src/constants.ts +72 -0
- package/src/doctor.ts +137 -0
- package/src/errors.ts +7 -0
- package/src/hook-executor.ts +196 -0
- package/src/init-tui.ts +193 -0
- package/src/init.ts +375 -0
- package/src/markdown.ts +73 -0
- package/src/normalize.ts +89 -0
- package/src/normalized-brief.ts +206 -0
- package/src/output-index.ts +59 -0
- package/src/paths.ts +72 -0
- package/src/preset-loader.ts +237 -0
- package/src/project-config.ts +78 -0
- package/src/providers/index.ts +12 -0
- package/src/providers/mock-provider.ts +188 -0
- package/src/providers/openai-provider.ts +87 -0
- package/src/registry.ts +119 -0
- package/src/settings.ts +34 -0
- package/src/template-resolver.ts +33 -0
- package/src/templates.ts +440 -0
- package/src/types.ts +46 -0
- package/src/utils.ts +50 -0
- package/src/validate.ts +246 -0
- package/src/validator.ts +96 -0
- package/src/version.ts +24 -0
- package/src/workflow-commands.ts +31 -0
- package/templates/artifacts/prd.md +219 -0
- package/templates/artifacts/stories.md +49 -0
- package/templates/artifacts/techspec.md +42 -0
- package/templates/artifacts/wireframe.html +260 -0
- package/templates/artifacts/wireframe.md +22 -0
- package/templates/artifacts/workflow.md +22 -0
- package/templates/artifacts/workflow.mmd +6 -0
- package/templates/commands/prodo-normalize.md +24 -0
- package/templates/commands/prodo-prd.md +24 -0
- package/templates/commands/prodo-stories.md +24 -0
- package/templates/commands/prodo-techspec.md +24 -0
- package/templates/commands/prodo-validate.md +24 -0
- package/templates/commands/prodo-wireframe.md +24 -0
- package/templates/commands/prodo-workflow.md +24 -0
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateArtifact = generateArtifact;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const gray_matter_1 = __importDefault(require("gray-matter"));
|
|
10
|
+
const constants_1 = require("./constants");
|
|
11
|
+
const artifact_registry_1 = require("./artifact-registry");
|
|
12
|
+
const errors_1 = require("./errors");
|
|
13
|
+
const normalized_brief_1 = require("./normalized-brief");
|
|
14
|
+
const output_index_1 = require("./output-index");
|
|
15
|
+
const paths_1 = require("./paths");
|
|
16
|
+
const providers_1 = require("./providers");
|
|
17
|
+
const template_resolver_1 = require("./template-resolver");
|
|
18
|
+
const settings_1 = require("./settings");
|
|
19
|
+
const markdown_1 = require("./markdown");
|
|
20
|
+
const utils_1 = require("./utils");
|
|
21
|
+
const validator_1 = require("./validator");
|
|
22
|
+
function defaultFilename(type) {
|
|
23
|
+
if (type === "workflow")
|
|
24
|
+
return `${type}-${(0, utils_1.timestampSlug)()}.md`;
|
|
25
|
+
if (type === "wireframe")
|
|
26
|
+
return `${type}-${(0, utils_1.timestampSlug)()}.md`;
|
|
27
|
+
return `${type}-${(0, utils_1.timestampSlug)()}.md`;
|
|
28
|
+
}
|
|
29
|
+
function sidecarPath(filePath) {
|
|
30
|
+
const parsed = node_path_1.default.parse(filePath);
|
|
31
|
+
return node_path_1.default.join(parsed.dir, `${parsed.name}.artifact.json`);
|
|
32
|
+
}
|
|
33
|
+
async function writeSidecar(filePath, doc) {
|
|
34
|
+
const payload = {
|
|
35
|
+
frontmatter: doc.frontmatter,
|
|
36
|
+
body: doc.body
|
|
37
|
+
};
|
|
38
|
+
await promises_1.default.writeFile(sidecarPath(filePath), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
39
|
+
}
|
|
40
|
+
async function loadArtifactDoc(filePath) {
|
|
41
|
+
const sidecar = sidecarPath(filePath);
|
|
42
|
+
if (await (0, utils_1.fileExists)(sidecar)) {
|
|
43
|
+
const loaded = await (0, utils_1.readJsonFile)(sidecar);
|
|
44
|
+
return {
|
|
45
|
+
frontmatter: loaded.frontmatter ?? {},
|
|
46
|
+
body: typeof loaded.body === "string" ? loaded.body : ""
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const raw = await promises_1.default.readFile(filePath, "utf8");
|
|
50
|
+
const parsed = (0, gray_matter_1.default)(raw);
|
|
51
|
+
return {
|
|
52
|
+
frontmatter: parsed.data,
|
|
53
|
+
body: parsed.content
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function hasEnglishLeak(body) {
|
|
57
|
+
const englishMarkers = [" the ", " and ", " with ", " user ", " should ", " must ", " requirement ", " flow "];
|
|
58
|
+
const normalized = ` ${body.toLowerCase().replace(/\s+/g, " ")} `;
|
|
59
|
+
return englishMarkers.filter((m) => normalized.includes(m)).length >= 2;
|
|
60
|
+
}
|
|
61
|
+
function enforceLanguage(body, lang, artifactType) {
|
|
62
|
+
const normalized = (lang || "en").toLowerCase();
|
|
63
|
+
if (!normalized.startsWith("tr"))
|
|
64
|
+
return;
|
|
65
|
+
if (hasEnglishLeak(body)) {
|
|
66
|
+
throw new errors_1.UserError(`Language enforcement failed for ${artifactType}: output contains English fragments while language is Turkish.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function toSlug(value) {
|
|
70
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "screen";
|
|
71
|
+
}
|
|
72
|
+
function extractTurkishTitle(featureText) {
|
|
73
|
+
const base = featureText.replace(/^\[[A-Z][0-9]+\]\s*/, "").trim();
|
|
74
|
+
if (!base)
|
|
75
|
+
return "Ekran";
|
|
76
|
+
return base;
|
|
77
|
+
}
|
|
78
|
+
async function resolvePrompt(cwd, artifactType, templateContent, requiredHeadings, agent) {
|
|
79
|
+
const base = await promises_1.default.readFile((0, paths_1.promptPath)(cwd, artifactType), "utf8");
|
|
80
|
+
const authority = `Template authority (STRICT):
|
|
81
|
+
- Treat this template as the single output structure source.
|
|
82
|
+
- Keep heading order and names exactly as listed.
|
|
83
|
+
- Do not invent new primary sections.
|
|
84
|
+
|
|
85
|
+
Required headings (from template):
|
|
86
|
+
${requiredHeadings.map((heading) => `- ${heading}`).join("\n")}
|
|
87
|
+
|
|
88
|
+
Resolved template:
|
|
89
|
+
\`\`\`md
|
|
90
|
+
${templateContent.trim()}
|
|
91
|
+
\`\`\``;
|
|
92
|
+
const workflowPairing = artifactType === "workflow"
|
|
93
|
+
? `
|
|
94
|
+
Workflow paired output contract (STRICT):
|
|
95
|
+
- Output markdown explanation first (template headings).
|
|
96
|
+
- Then append a mermaid block for the same flow:
|
|
97
|
+
\`\`\`mermaid
|
|
98
|
+
flowchart TD
|
|
99
|
+
...
|
|
100
|
+
\`\`\`
|
|
101
|
+
- Mermaid block is mandatory.`
|
|
102
|
+
: "";
|
|
103
|
+
const withTemplate = `${base}
|
|
104
|
+
|
|
105
|
+
${authority}${workflowPairing}`;
|
|
106
|
+
if (!agent)
|
|
107
|
+
return withTemplate;
|
|
108
|
+
return `${withTemplate}
|
|
109
|
+
|
|
110
|
+
Agent execution profile: ${agent}
|
|
111
|
+
- Keep output deterministic and actionable.`;
|
|
112
|
+
}
|
|
113
|
+
async function loadLatestArtifactPath(cwd, type) {
|
|
114
|
+
const def = await (0, artifact_registry_1.getArtifactDefinition)(cwd, type);
|
|
115
|
+
const active = await (0, output_index_1.getActiveArtifactPath)(cwd, type);
|
|
116
|
+
if (active)
|
|
117
|
+
return active;
|
|
118
|
+
const files = await (0, utils_1.listFilesSortedByMtime)((0, paths_1.outputDirPath)(cwd, type, def.output_dir));
|
|
119
|
+
return files[0];
|
|
120
|
+
}
|
|
121
|
+
function contextFilePath(cwd, artifactFile) {
|
|
122
|
+
const base = node_path_1.default.parse(artifactFile).name;
|
|
123
|
+
return node_path_1.default.join((0, paths_1.outputContextDirPath)(cwd), `${base}.json`);
|
|
124
|
+
}
|
|
125
|
+
function toLineItems(value) {
|
|
126
|
+
if (!value)
|
|
127
|
+
return [];
|
|
128
|
+
return value
|
|
129
|
+
.split(/\r?\n/)
|
|
130
|
+
.map((line) => line.replace(/^\s*[-*0-9.]+\s*/, "").trim())
|
|
131
|
+
.filter((line) => line.length > 0);
|
|
132
|
+
}
|
|
133
|
+
function parseHeadingTitle(fullHeading) {
|
|
134
|
+
return fullHeading.replace(/^##\s+/, "").trim();
|
|
135
|
+
}
|
|
136
|
+
function deriveStructuredContext(artifactType, body, requiredHeadings) {
|
|
137
|
+
const sections = (0, markdown_1.sectionTextMap)(body);
|
|
138
|
+
const ordered = requiredHeadings
|
|
139
|
+
.map((heading) => ({ heading, items: toLineItems(sections.get(heading)) }))
|
|
140
|
+
.filter((item) => item.items.length > 0);
|
|
141
|
+
const section_map = Object.fromEntries(Array.from(sections.entries()).map(([heading, text]) => [parseHeadingTitle(heading), toLineItems(text)]));
|
|
142
|
+
if (artifactType === "workflow") {
|
|
143
|
+
return {
|
|
144
|
+
section_map,
|
|
145
|
+
actor_map: ordered[0]?.items ?? [],
|
|
146
|
+
step_map: ordered[1]?.items ?? [],
|
|
147
|
+
edge_case_map: ordered[2]?.items ?? []
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (artifactType === "wireframe") {
|
|
151
|
+
return {
|
|
152
|
+
section_map,
|
|
153
|
+
screen_map: ordered[0]?.items ?? [],
|
|
154
|
+
interaction_map: ordered[1]?.items ?? []
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (artifactType === "techspec") {
|
|
158
|
+
return {
|
|
159
|
+
section_map,
|
|
160
|
+
architecture_map: ordered[0]?.items ?? [],
|
|
161
|
+
integration_map: ordered[1]?.items ?? []
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (artifactType === "stories") {
|
|
165
|
+
return {
|
|
166
|
+
section_map,
|
|
167
|
+
story_map: ordered[0]?.items ?? [],
|
|
168
|
+
acceptance_map: ordered[1]?.items ?? []
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
section_map,
|
|
173
|
+
goal_map: ordered[0]?.items ?? [],
|
|
174
|
+
requirement_map: ordered[1]?.items ?? []
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async function buildUpstreamArtifacts(cwd, artifactType, upstreamTypes) {
|
|
178
|
+
const refs = [];
|
|
179
|
+
for (const type of upstreamTypes) {
|
|
180
|
+
const latest = await loadLatestArtifactPath(cwd, type);
|
|
181
|
+
if (!latest)
|
|
182
|
+
continue;
|
|
183
|
+
const parsed = await loadArtifactDoc(latest);
|
|
184
|
+
const frontmatter = parsed.frontmatter;
|
|
185
|
+
const coverageRaw = frontmatter.contract_coverage;
|
|
186
|
+
const contextPath = contextFilePath(cwd, latest);
|
|
187
|
+
const structuredContext = (await (0, utils_1.fileExists)(contextPath))
|
|
188
|
+
? await (0, utils_1.readJsonFile)(contextPath)
|
|
189
|
+
: {};
|
|
190
|
+
refs.push({
|
|
191
|
+
type,
|
|
192
|
+
file: latest,
|
|
193
|
+
contractCoverage: {
|
|
194
|
+
goals: Array.isArray(coverageRaw?.goals)
|
|
195
|
+
? coverageRaw.goals.filter((item) => typeof item === "string")
|
|
196
|
+
: [],
|
|
197
|
+
core_features: Array.isArray(coverageRaw?.core_features)
|
|
198
|
+
? coverageRaw.core_features.filter((item) => typeof item === "string")
|
|
199
|
+
: [],
|
|
200
|
+
constraints: Array.isArray(coverageRaw?.constraints)
|
|
201
|
+
? coverageRaw.constraints.filter((item) => typeof item === "string")
|
|
202
|
+
: []
|
|
203
|
+
},
|
|
204
|
+
...(Object.keys(structuredContext).length > 0 ? { structuredContext } : {})
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return refs;
|
|
208
|
+
}
|
|
209
|
+
function extractCoverageFromBody(body) {
|
|
210
|
+
const tagged = {
|
|
211
|
+
goals: Array.from(new Set(body.match(/\[(G[0-9]+)\]/g)?.map((item) => item.slice(1, -1)) ?? [])),
|
|
212
|
+
core_features: Array.from(new Set(body.match(/\[(F[0-9]+)\]/g)?.map((item) => item.slice(1, -1)) ?? [])),
|
|
213
|
+
constraints: Array.from(new Set(body.match(/\[(C[0-9]+)\]/g)?.map((item) => item.slice(1, -1)) ?? []))
|
|
214
|
+
};
|
|
215
|
+
return tagged;
|
|
216
|
+
}
|
|
217
|
+
function missingCoverage(requiredContracts, normalized, coverage) {
|
|
218
|
+
const ids = (0, normalized_brief_1.contractIds)(normalized.contracts);
|
|
219
|
+
const missing = [];
|
|
220
|
+
for (const key of requiredContracts) {
|
|
221
|
+
const expected = ids[key];
|
|
222
|
+
if (expected.length === 0)
|
|
223
|
+
continue;
|
|
224
|
+
const missingIds = expected.filter((id) => !coverage[key].includes(id));
|
|
225
|
+
if (missingIds.length > 0) {
|
|
226
|
+
missing.push({ key, ids: missingIds });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return missing;
|
|
230
|
+
}
|
|
231
|
+
async function ensurePipelinePrereqs(cwd, normalizedPath) {
|
|
232
|
+
const prodoRoot = (0, paths_1.prodoPath)(cwd);
|
|
233
|
+
if (!(await (0, utils_1.fileExists)(prodoRoot))) {
|
|
234
|
+
throw new errors_1.UserError("Missing .prodo directory. Run `prodo-init` first.");
|
|
235
|
+
}
|
|
236
|
+
if (!(await (0, utils_1.fileExists)((0, paths_1.briefPath)(cwd)))) {
|
|
237
|
+
throw new errors_1.UserError("Missing brief at `brief.md`. Run `prodo-init` or create the file.");
|
|
238
|
+
}
|
|
239
|
+
if (!(await (0, utils_1.fileExists)(normalizedPath))) {
|
|
240
|
+
throw new errors_1.UserError("Missing normalized brief at `.prodo/briefs/normalized-brief.json`. Create it before generating artifacts.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function splitWorkflowPair(raw) {
|
|
244
|
+
const match = raw.match(/```mermaid\s*([\s\S]*?)```/i);
|
|
245
|
+
if (!match) {
|
|
246
|
+
throw new errors_1.UserError("Workflow output is missing a Mermaid block. Regenerate with template-compliant paired output.");
|
|
247
|
+
}
|
|
248
|
+
const mermaid = match[1].trim();
|
|
249
|
+
const markdown = raw.replace(match[0], "").trim();
|
|
250
|
+
if (!markdown) {
|
|
251
|
+
throw new errors_1.UserError("Workflow markdown explanation is empty.");
|
|
252
|
+
}
|
|
253
|
+
if (!/(^|\n)\s*(flowchart|graph)\s+/i.test(mermaid)) {
|
|
254
|
+
throw new errors_1.UserError("Workflow Mermaid block is invalid.");
|
|
255
|
+
}
|
|
256
|
+
return { markdown, mermaid };
|
|
257
|
+
}
|
|
258
|
+
async function writeWireframeScreens(targetDir, baseName, normalized, coverage, lang, headings) {
|
|
259
|
+
const tr = lang.toLowerCase().startsWith("tr");
|
|
260
|
+
const screenContracts = normalized.contracts.core_features
|
|
261
|
+
.filter((item) => coverage.core_features.includes(item.id))
|
|
262
|
+
.slice(0, 6);
|
|
263
|
+
const screens = screenContracts.length > 0 ? screenContracts : normalized.contracts.core_features.slice(0, 3);
|
|
264
|
+
const summaryBodies = [];
|
|
265
|
+
let primaryMdPath = "";
|
|
266
|
+
for (const [index, screen] of screens.entries()) {
|
|
267
|
+
const title = extractTurkishTitle(screen.text);
|
|
268
|
+
const screenBase = `${baseName}-${index + 1}-${toSlug(title)}`;
|
|
269
|
+
const htmlPath = node_path_1.default.join(targetDir, `${screenBase}.html`);
|
|
270
|
+
const mdPath = node_path_1.default.join(targetDir, `${screenBase}.md`);
|
|
271
|
+
const html = `<!doctype html>
|
|
272
|
+
<html lang="${lang}">
|
|
273
|
+
<head>
|
|
274
|
+
<meta charset="utf-8" />
|
|
275
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
276
|
+
<title>${title}</title>
|
|
277
|
+
</head>
|
|
278
|
+
<body>
|
|
279
|
+
<!-- [${screen.id}] -->
|
|
280
|
+
<header>
|
|
281
|
+
<h1>${title}</h1>
|
|
282
|
+
<nav><button type="button">${tr ? "Geri" : "Back"}</button><button type="button">${tr ? "Devam" : "Next"}</button></nav>
|
|
283
|
+
</header>
|
|
284
|
+
<main>
|
|
285
|
+
<section>
|
|
286
|
+
<h2>${tr ? "Icerik" : "Content"}</h2>
|
|
287
|
+
<ul>
|
|
288
|
+
<li>${tr ? "Birincil bilgi alani" : "Primary information area"}</li>
|
|
289
|
+
<li>${tr ? "Durum gostergesi" : "Status indicator"}</li>
|
|
290
|
+
</ul>
|
|
291
|
+
</section>
|
|
292
|
+
<section>
|
|
293
|
+
<h2>${tr ? "Form" : "Form"}</h2>
|
|
294
|
+
<form>
|
|
295
|
+
<label>${tr ? "Alan" : "Field"}
|
|
296
|
+
<input type="text" name="field_${index + 1}" />
|
|
297
|
+
</label>
|
|
298
|
+
<button type="submit">${tr ? "Kaydet" : "Save"}</button>
|
|
299
|
+
</form>
|
|
300
|
+
</section>
|
|
301
|
+
</main>
|
|
302
|
+
</body>
|
|
303
|
+
</html>
|
|
304
|
+
`;
|
|
305
|
+
await promises_1.default.writeFile(htmlPath, html, "utf8");
|
|
306
|
+
const defaultMap = {
|
|
307
|
+
purpose: [`- [${screen.id}] ${screen.text}`],
|
|
308
|
+
actor: [`- ${(normalized.audience[0] ?? (tr ? "Birincil kullanici" : "Primary user"))}`],
|
|
309
|
+
sections: [
|
|
310
|
+
`- ${tr ? "Baslik ve gezinme" : "Header and navigation"}`,
|
|
311
|
+
`- ${tr ? "Icerik bolumu" : "Content section"}`,
|
|
312
|
+
`- ${tr ? "Form bolumu" : "Form section"}`
|
|
313
|
+
],
|
|
314
|
+
fields: [`- ${tr ? "Metin alani (field_" : "Text input (field_"}${index + 1})`],
|
|
315
|
+
actions: [`- ${tr ? "Geri" : "Back"}`, `- ${tr ? "Devam" : "Next"}`, `- ${tr ? "Kaydet" : "Save"}`],
|
|
316
|
+
states: [`- ${tr ? "Bos durum, yukleniyor, hata, basari" : "Empty, loading, error, success states"}`],
|
|
317
|
+
notes: [`- ${tr ? "Dusuk sadakatli tel kafes taslaktir." : "Low-fidelity black-and-white wireframe mock."}`]
|
|
318
|
+
};
|
|
319
|
+
const fallbackQueue = [
|
|
320
|
+
...defaultMap.purpose,
|
|
321
|
+
...defaultMap.actor,
|
|
322
|
+
...defaultMap.sections,
|
|
323
|
+
...defaultMap.fields,
|
|
324
|
+
...defaultMap.actions,
|
|
325
|
+
...defaultMap.states,
|
|
326
|
+
...defaultMap.notes
|
|
327
|
+
];
|
|
328
|
+
const consumeFallback = () => (fallbackQueue.shift() ?? `- ${tr ? "Detay bekleniyor." : "Detail pending."}`);
|
|
329
|
+
const contentForHeading = (heading) => {
|
|
330
|
+
const key = heading.toLowerCase();
|
|
331
|
+
if (/(screen purpose|purpose|amac|hedef)/.test(key))
|
|
332
|
+
return defaultMap.purpose;
|
|
333
|
+
if (/(primary actor|actor|user|kullanici|rol)/.test(key))
|
|
334
|
+
return defaultMap.actor;
|
|
335
|
+
if (/(main section|section|bolum|layout)/.test(key))
|
|
336
|
+
return defaultMap.sections;
|
|
337
|
+
if (/(field|input|form|alan)/.test(key))
|
|
338
|
+
return defaultMap.fields;
|
|
339
|
+
if (/(action|button|cta|aksiyon)/.test(key))
|
|
340
|
+
return defaultMap.actions;
|
|
341
|
+
if (/(state|message|durum|mesaj)/.test(key))
|
|
342
|
+
return defaultMap.states;
|
|
343
|
+
if (/(note|not|aciklama)/.test(key))
|
|
344
|
+
return defaultMap.notes;
|
|
345
|
+
return [consumeFallback()];
|
|
346
|
+
};
|
|
347
|
+
const targetHeadings = headings.length > 0 ? headings : (0, constants_1.defaultRequiredHeadings)("wireframe");
|
|
348
|
+
const mdLines = [`# ${title}`, ""];
|
|
349
|
+
for (const heading of targetHeadings) {
|
|
350
|
+
mdLines.push(heading);
|
|
351
|
+
mdLines.push(...contentForHeading(heading));
|
|
352
|
+
mdLines.push("");
|
|
353
|
+
}
|
|
354
|
+
const mdBody = mdLines.join("\n").trim();
|
|
355
|
+
await promises_1.default.writeFile(mdPath, `${mdBody}\n`, "utf8");
|
|
356
|
+
if (!primaryMdPath)
|
|
357
|
+
primaryMdPath = mdPath;
|
|
358
|
+
summaryBodies.push(mdBody);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
primaryPath: primaryMdPath,
|
|
362
|
+
summaryBody: summaryBodies.join("\n\n")
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async function generateArtifact(options) {
|
|
366
|
+
const { cwd, artifactType, outPath, agent } = options;
|
|
367
|
+
const def = await (0, artifact_registry_1.getArtifactDefinition)(cwd, artifactType);
|
|
368
|
+
const normalizedPath = options.normalizedBriefOverride ?? (0, paths_1.normalizedBriefPath)(cwd);
|
|
369
|
+
await ensurePipelinePrereqs(cwd, normalizedPath);
|
|
370
|
+
const settings = await (0, settings_1.readSettings)(cwd);
|
|
371
|
+
const normalizedBriefRaw = await (0, utils_1.readJsonFile)(normalizedPath);
|
|
372
|
+
const normalizedBrief = (0, normalized_brief_1.parseNormalizedBriefOrThrow)(normalizedBriefRaw);
|
|
373
|
+
const template = await (0, template_resolver_1.resolveTemplate)({ cwd, artifactType });
|
|
374
|
+
if (!template || template.content.trim().length === 0) {
|
|
375
|
+
throw new errors_1.UserError(`Missing ${artifactType} template. Create \`.prodo/templates/${artifactType}.md\` before running \`prodo-${artifactType}\`.`);
|
|
376
|
+
}
|
|
377
|
+
const templateHeadings = template && template.content.trim().length > 0 ? (0, template_resolver_1.extractRequiredHeadingsFromTemplate)(template.content) : [];
|
|
378
|
+
if (templateHeadings.length === 0) {
|
|
379
|
+
throw new errors_1.UserError(`${artifactType} template has no extractable headings. Add markdown headings to \`${template.path}\`.`);
|
|
380
|
+
}
|
|
381
|
+
const computedHeadings = templateHeadings.length > 0
|
|
382
|
+
? templateHeadings
|
|
383
|
+
: (def.required_headings.length > 0 ? def.required_headings : (0, constants_1.defaultRequiredHeadings)(artifactType));
|
|
384
|
+
const prompt = await resolvePrompt(cwd, artifactType, template?.content ?? "", computedHeadings, agent);
|
|
385
|
+
const provider = (0, providers_1.createProvider)();
|
|
386
|
+
const upstreamArtifacts = await buildUpstreamArtifacts(cwd, artifactType, def.upstream);
|
|
387
|
+
const schemaHint = {
|
|
388
|
+
artifactType,
|
|
389
|
+
requiredHeadings: computedHeadings,
|
|
390
|
+
requiredContracts: def.required_contracts
|
|
391
|
+
};
|
|
392
|
+
const generated = await provider.generate(prompt, {
|
|
393
|
+
normalizedBrief,
|
|
394
|
+
upstreamArtifacts,
|
|
395
|
+
contractCatalog: normalizedBrief.contracts,
|
|
396
|
+
templateContent: template?.content ?? "",
|
|
397
|
+
templatePath: template?.path ?? "",
|
|
398
|
+
outputLanguage: settings.lang
|
|
399
|
+
}, schemaHint);
|
|
400
|
+
let generatedBody = generated.body.trim();
|
|
401
|
+
let workflowMermaidBody = null;
|
|
402
|
+
if (artifactType === "workflow") {
|
|
403
|
+
const paired = splitWorkflowPair(generatedBody);
|
|
404
|
+
generatedBody = paired.markdown;
|
|
405
|
+
workflowMermaidBody = paired.mermaid;
|
|
406
|
+
}
|
|
407
|
+
let contractCoverage = extractCoverageFromBody(generatedBody);
|
|
408
|
+
if (artifactType === "workflow") {
|
|
409
|
+
if (contractCoverage.core_features.length === 0) {
|
|
410
|
+
contractCoverage = {
|
|
411
|
+
...contractCoverage,
|
|
412
|
+
core_features: normalizedBrief.contracts.core_features.map((item) => item.id)
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (artifactType === "wireframe") {
|
|
417
|
+
if (contractCoverage.core_features.length === 0) {
|
|
418
|
+
contractCoverage = {
|
|
419
|
+
...contractCoverage,
|
|
420
|
+
core_features: normalizedBrief.contracts.core_features.map((item) => item.id)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
enforceLanguage(generatedBody, settings.lang, artifactType);
|
|
425
|
+
const uncovered = missingCoverage(def.required_contracts, normalizedBrief, contractCoverage);
|
|
426
|
+
if (uncovered.length > 0) {
|
|
427
|
+
const lines = uncovered
|
|
428
|
+
.map((item) => `- ${item.key}: missing ${item.ids.join(", ")}`)
|
|
429
|
+
.join("\n");
|
|
430
|
+
throw new errors_1.UserError(`Artifact is missing required contract references. Add ID tags to body:\n${lines}\nExample tags: [G1], [F2], [C1].`);
|
|
431
|
+
}
|
|
432
|
+
const frontmatter = {
|
|
433
|
+
artifact_type: artifactType,
|
|
434
|
+
version: (0, utils_1.timestampSlug)(),
|
|
435
|
+
source_brief: node_path_1.default.resolve(normalizedPath),
|
|
436
|
+
generated_at: new Date().toISOString(),
|
|
437
|
+
status: constants_1.DEFAULT_STATUS,
|
|
438
|
+
upstream_artifacts: upstreamArtifacts.map((item) => item.file),
|
|
439
|
+
contract_coverage: contractCoverage,
|
|
440
|
+
language: settings.lang
|
|
441
|
+
};
|
|
442
|
+
const mergedFrontmatter = { ...frontmatter, ...(generated.frontmatter ?? {}) };
|
|
443
|
+
let doc = {
|
|
444
|
+
frontmatter: mergedFrontmatter,
|
|
445
|
+
body: generatedBody
|
|
446
|
+
};
|
|
447
|
+
const validation = await (0, validator_1.validateSchema)(cwd, artifactType, doc, schemaHint.requiredHeadings);
|
|
448
|
+
const schemaErrors = validation.issues.filter((issue) => issue.level === "error");
|
|
449
|
+
if (schemaErrors.length > 0) {
|
|
450
|
+
const details = schemaErrors.map((issue) => `- ${issue.message}`).join("\n");
|
|
451
|
+
throw new errors_1.UserError(`Artifact failed schema checks:\n${details}`);
|
|
452
|
+
}
|
|
453
|
+
const targetDir = (0, paths_1.outputDirPath)(cwd, artifactType, def.output_dir);
|
|
454
|
+
const finalPath = outPath ? node_path_1.default.resolve(cwd, outPath) : node_path_1.default.join(targetDir, defaultFilename(artifactType));
|
|
455
|
+
if (!(0, utils_1.isPathInside)(node_path_1.default.join(cwd, "product-docs"), finalPath)) {
|
|
456
|
+
throw new errors_1.UserError("Artifact output must be inside `product-docs/`.");
|
|
457
|
+
}
|
|
458
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(finalPath), { recursive: true });
|
|
459
|
+
if (artifactType === "workflow") {
|
|
460
|
+
const basePath = node_path_1.default.join(node_path_1.default.dirname(finalPath), node_path_1.default.parse(finalPath).name);
|
|
461
|
+
const mdPath = `${basePath}.md`;
|
|
462
|
+
const mmdPath = `${basePath}.mmd`;
|
|
463
|
+
await promises_1.default.writeFile(mdPath, gray_matter_1.default.stringify(doc.body, doc.frontmatter), "utf8");
|
|
464
|
+
await promises_1.default.writeFile(mmdPath, `${(workflowMermaidBody ?? "").trim()}\n`, "utf8");
|
|
465
|
+
await writeSidecar(mdPath, doc);
|
|
466
|
+
const derivedContext = {
|
|
467
|
+
artifact_type: artifactType,
|
|
468
|
+
artifact_file: mdPath,
|
|
469
|
+
generated_at: new Date().toISOString(),
|
|
470
|
+
contract_coverage: contractCoverage,
|
|
471
|
+
...deriveStructuredContext(artifactType, doc.body, schemaHint.requiredHeadings)
|
|
472
|
+
};
|
|
473
|
+
await promises_1.default.mkdir((0, paths_1.outputContextDirPath)(cwd), { recursive: true });
|
|
474
|
+
await promises_1.default.writeFile(contextFilePath(cwd, mdPath), `${JSON.stringify(derivedContext, null, 2)}\n`, "utf8");
|
|
475
|
+
await (0, output_index_1.setActiveArtifact)(cwd, artifactType, mdPath);
|
|
476
|
+
return mdPath;
|
|
477
|
+
}
|
|
478
|
+
else if (artifactType === "wireframe") {
|
|
479
|
+
const base = node_path_1.default.parse(finalPath).name;
|
|
480
|
+
const wireframe = await writeWireframeScreens(node_path_1.default.dirname(finalPath), base, normalizedBrief, contractCoverage, settings.lang, schemaHint.requiredHeadings);
|
|
481
|
+
doc = {
|
|
482
|
+
frontmatter: doc.frontmatter,
|
|
483
|
+
body: wireframe.summaryBody
|
|
484
|
+
};
|
|
485
|
+
await writeSidecar(wireframe.primaryPath, doc);
|
|
486
|
+
const derivedContext = {
|
|
487
|
+
artifact_type: artifactType,
|
|
488
|
+
artifact_file: wireframe.primaryPath,
|
|
489
|
+
generated_at: new Date().toISOString(),
|
|
490
|
+
contract_coverage: contractCoverage,
|
|
491
|
+
...deriveStructuredContext(artifactType, doc.body, schemaHint.requiredHeadings)
|
|
492
|
+
};
|
|
493
|
+
await promises_1.default.mkdir((0, paths_1.outputContextDirPath)(cwd), { recursive: true });
|
|
494
|
+
await promises_1.default.writeFile(contextFilePath(cwd, wireframe.primaryPath), `${JSON.stringify(derivedContext, null, 2)}\n`, "utf8");
|
|
495
|
+
await (0, output_index_1.setActiveArtifact)(cwd, artifactType, wireframe.primaryPath);
|
|
496
|
+
return wireframe.primaryPath;
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const content = gray_matter_1.default.stringify(doc.body, doc.frontmatter);
|
|
500
|
+
await promises_1.default.writeFile(finalPath, content, "utf8");
|
|
501
|
+
}
|
|
502
|
+
await writeSidecar(finalPath, doc);
|
|
503
|
+
const derivedContext = {
|
|
504
|
+
artifact_type: artifactType,
|
|
505
|
+
artifact_file: finalPath,
|
|
506
|
+
generated_at: new Date().toISOString(),
|
|
507
|
+
contract_coverage: contractCoverage,
|
|
508
|
+
...deriveStructuredContext(artifactType, doc.body, schemaHint.requiredHeadings)
|
|
509
|
+
};
|
|
510
|
+
await promises_1.default.mkdir((0, paths_1.outputContextDirPath)(cwd), { recursive: true });
|
|
511
|
+
await promises_1.default.writeFile(contextFilePath(cwd, finalPath), `${JSON.stringify(derivedContext, null, 2)}\n`, "utf8");
|
|
512
|
+
await (0, output_index_1.setActiveArtifact)(cwd, artifactType, finalPath);
|
|
513
|
+
return finalPath;
|
|
514
|
+
}
|
package/dist/cli.d.ts
ADDED