@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
package/src/validate.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { listArtifactDefinitions } from "./artifact-registry";
|
|
5
|
+
import { checkConsistency } from "./consistency";
|
|
6
|
+
import { UserError } from "./errors";
|
|
7
|
+
import { getActiveArtifactPath } from "./output-index";
|
|
8
|
+
import { normalizedBriefPath, outputDirPath, reportPath } from "./paths";
|
|
9
|
+
import { extractRequiredHeadingsFromTemplate, resolveTemplate } from "./template-resolver";
|
|
10
|
+
import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
|
|
11
|
+
import { ensureDir, fileExists, isPathInside, listFilesSortedByMtime, readJsonFile } from "./utils";
|
|
12
|
+
import { validateSchema } from "./validator";
|
|
13
|
+
|
|
14
|
+
type LoadedArtifact = {
|
|
15
|
+
type: ArtifactType;
|
|
16
|
+
file: string;
|
|
17
|
+
doc: ArtifactDoc;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function sidecarPath(filePath: string): string {
|
|
21
|
+
const parsed = path.parse(filePath);
|
|
22
|
+
return path.join(parsed.dir, `${parsed.name}.artifact.json`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function loadArtifactDoc(filePath: string): Promise<ArtifactDoc> {
|
|
26
|
+
const sidecar = sidecarPath(filePath);
|
|
27
|
+
if (await fileExists(sidecar)) {
|
|
28
|
+
const payload = await readJsonFile<Record<string, unknown>>(sidecar);
|
|
29
|
+
return {
|
|
30
|
+
frontmatter: (payload.frontmatter as Record<string, unknown>) ?? {},
|
|
31
|
+
body: typeof payload.body === "string" ? payload.body : ""
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
35
|
+
const parsed = matter(raw);
|
|
36
|
+
return {
|
|
37
|
+
frontmatter: parsed.data as Record<string, unknown>,
|
|
38
|
+
body: parsed.content
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function loadLatestArtifacts(cwd: string): Promise<LoadedArtifact[]> {
|
|
43
|
+
const defs = await listArtifactDefinitions(cwd);
|
|
44
|
+
const loaded: LoadedArtifact[] = [];
|
|
45
|
+
for (const def of defs) {
|
|
46
|
+
const type = def.name;
|
|
47
|
+
const active = await getActiveArtifactPath(cwd, type);
|
|
48
|
+
const fallback = async (): Promise<string | undefined> => {
|
|
49
|
+
const files = await listFilesSortedByMtime(outputDirPath(cwd, type, def.output_dir));
|
|
50
|
+
return files[0];
|
|
51
|
+
};
|
|
52
|
+
const latest = active ?? (await fallback());
|
|
53
|
+
if (!latest) continue;
|
|
54
|
+
const parsed = await loadArtifactDoc(latest);
|
|
55
|
+
loaded.push({
|
|
56
|
+
type,
|
|
57
|
+
file: latest,
|
|
58
|
+
doc: parsed
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return loaded;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function listHtmlFiles(dir: string): Promise<string[]> {
|
|
65
|
+
if (!(await fileExists(dir))) return [];
|
|
66
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
67
|
+
return entries
|
|
68
|
+
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".html"))
|
|
69
|
+
.map((entry) => path.join(dir, entry.name));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatIssue(issue: ValidationIssue): string {
|
|
73
|
+
const location = issue.file ? ` (${issue.file})` : "";
|
|
74
|
+
const field = issue.field ? ` [${issue.field}]` : "";
|
|
75
|
+
const check = issue.check ? ` [${issue.check}]` : "";
|
|
76
|
+
const fix = issue.suggestion ? `\n Suggestion: ${issue.suggestion}` : "";
|
|
77
|
+
return `- [${issue.level.toUpperCase()}] ${issue.code}${check}${field}: ${issue.message}${location}${fix}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function writeReport(targetPath: string, issues: ValidationIssue[]): Promise<void> {
|
|
81
|
+
const status = issues.some((issue) => issue.level === "error") ? "FAIL" : "PASS";
|
|
82
|
+
const schemaIssues = issues.filter((issue) => issue.check === "schema");
|
|
83
|
+
const coverageIssues = issues.filter((issue) => issue.check === "tag_coverage");
|
|
84
|
+
const relevanceIssues = issues.filter((issue) => issue.check === "contract_relevance");
|
|
85
|
+
const semanticIssues = issues.filter((issue) => issue.check === "semantic_consistency");
|
|
86
|
+
const gate = (set: ValidationIssue[]) => (set.some((issue) => issue.level === "error") ? "FAIL" : "PASS");
|
|
87
|
+
const content = [
|
|
88
|
+
"# Prodo Validation Report",
|
|
89
|
+
"",
|
|
90
|
+
`Status: **${status}**`,
|
|
91
|
+
`Generated at: ${new Date().toISOString()}`,
|
|
92
|
+
"",
|
|
93
|
+
"## Gate Results",
|
|
94
|
+
`- Schema pass: ${gate(schemaIssues)}`,
|
|
95
|
+
`- Tag coverage pass: ${gate(coverageIssues)}`,
|
|
96
|
+
`- Contract relevance pass: ${gate(relevanceIssues)}`,
|
|
97
|
+
`- Semantic consistency pass: ${gate(semanticIssues)}`,
|
|
98
|
+
"",
|
|
99
|
+
"## Findings",
|
|
100
|
+
issues.length === 0 ? "- No issues found." : issues.map(formatIssue).join("\n"),
|
|
101
|
+
""
|
|
102
|
+
].join("\n");
|
|
103
|
+
|
|
104
|
+
await ensureDir(path.dirname(targetPath));
|
|
105
|
+
await fs.writeFile(targetPath, content, "utf8");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function runValidate(
|
|
109
|
+
cwd: string,
|
|
110
|
+
options: { strict?: boolean; report?: string }
|
|
111
|
+
): Promise<{ pass: boolean; reportPath: string; issues: ValidationIssue[] }> {
|
|
112
|
+
const normalizedPath = normalizedBriefPath(cwd);
|
|
113
|
+
if (!(await fileExists(normalizedPath))) {
|
|
114
|
+
throw new UserError("Missing `.prodo/briefs/normalized-brief.json`. Run `prodo-init` and create it first.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalizedBrief = await readJsonFile<Record<string, unknown>>(normalizedPath);
|
|
118
|
+
const loaded = await loadLatestArtifacts(cwd);
|
|
119
|
+
const issues: ValidationIssue[] = [];
|
|
120
|
+
|
|
121
|
+
for (const artifact of loaded) {
|
|
122
|
+
const template = await resolveTemplate({
|
|
123
|
+
cwd,
|
|
124
|
+
artifactType: artifact.type
|
|
125
|
+
});
|
|
126
|
+
const headings = template ? extractRequiredHeadingsFromTemplate(template.content) : [];
|
|
127
|
+
const schemaCheck = await validateSchema(
|
|
128
|
+
cwd,
|
|
129
|
+
artifact.type,
|
|
130
|
+
artifact.doc,
|
|
131
|
+
headings
|
|
132
|
+
);
|
|
133
|
+
issues.push(...schemaCheck.issues.map((issue) => ({ ...issue, file: artifact.file })));
|
|
134
|
+
|
|
135
|
+
if (artifact.type === "workflow") {
|
|
136
|
+
const ext = path.extname(artifact.file).toLowerCase();
|
|
137
|
+
if (ext !== ".md") {
|
|
138
|
+
issues.push({
|
|
139
|
+
level: "error",
|
|
140
|
+
code: "workflow_markdown_missing",
|
|
141
|
+
check: "schema",
|
|
142
|
+
artifactType: artifact.type,
|
|
143
|
+
file: artifact.file,
|
|
144
|
+
message: "Workflow explanation artifact must be Markdown (.md).",
|
|
145
|
+
suggestion: "Regenerate workflow so explanation is written to .md."
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const mmdPath = path.join(path.dirname(artifact.file), `${path.parse(artifact.file).name}.mmd`);
|
|
149
|
+
if (!(await fileExists(mmdPath))) {
|
|
150
|
+
issues.push({
|
|
151
|
+
level: "error",
|
|
152
|
+
code: "workflow_mermaid_missing",
|
|
153
|
+
check: "schema",
|
|
154
|
+
artifactType: artifact.type,
|
|
155
|
+
file: artifact.file,
|
|
156
|
+
message: "Workflow Mermaid companion file (.mmd) is missing.",
|
|
157
|
+
suggestion: "Regenerate workflow so markdown and .mmd are produced as a pair."
|
|
158
|
+
});
|
|
159
|
+
} else {
|
|
160
|
+
const mmdRaw = await fs.readFile(mmdPath, "utf8");
|
|
161
|
+
const mermaidLike = /(^|\n)\s*flowchart\s+/i.test(mmdRaw) || /(^|\n)\s*graph\s+/i.test(mmdRaw);
|
|
162
|
+
if (!mermaidLike) {
|
|
163
|
+
issues.push({
|
|
164
|
+
level: "error",
|
|
165
|
+
code: "workflow_mermaid_invalid",
|
|
166
|
+
check: "schema",
|
|
167
|
+
artifactType: artifact.type,
|
|
168
|
+
file: mmdPath,
|
|
169
|
+
message: "Workflow Mermaid file is invalid or prose-only.",
|
|
170
|
+
suggestion: "Ensure .mmd file contains valid Mermaid diagram syntax."
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (artifact.type === "wireframe") {
|
|
177
|
+
const ext = path.extname(artifact.file).toLowerCase();
|
|
178
|
+
if (ext !== ".md") {
|
|
179
|
+
issues.push({
|
|
180
|
+
level: "error",
|
|
181
|
+
code: "wireframe_markdown_missing",
|
|
182
|
+
check: "schema",
|
|
183
|
+
artifactType: artifact.type,
|
|
184
|
+
file: artifact.file,
|
|
185
|
+
message: "Wireframe explanation artifact must be Markdown (.md).",
|
|
186
|
+
suggestion: "Regenerate wireframe so explanation is written to .md."
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const htmlPath = path.join(path.dirname(artifact.file), `${path.parse(artifact.file).name}.html`);
|
|
191
|
+
if (!(await fileExists(htmlPath))) {
|
|
192
|
+
issues.push({
|
|
193
|
+
level: "error",
|
|
194
|
+
code: "wireframe_html_missing",
|
|
195
|
+
check: "schema",
|
|
196
|
+
artifactType: artifact.type,
|
|
197
|
+
file: artifact.file,
|
|
198
|
+
message: "Wireframe HTML companion file is missing.",
|
|
199
|
+
suggestion: "Regenerate wireframe so markdown and .html are produced as a pair."
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
const htmlRaw = await fs.readFile(htmlPath, "utf8");
|
|
203
|
+
const htmlLooksValid = /<!doctype html>/i.test(htmlRaw) || /<html[\s>]/i.test(htmlRaw);
|
|
204
|
+
if (!htmlLooksValid) {
|
|
205
|
+
issues.push({
|
|
206
|
+
level: "error",
|
|
207
|
+
code: "wireframe_html_invalid",
|
|
208
|
+
check: "schema",
|
|
209
|
+
artifactType: artifact.type,
|
|
210
|
+
file: htmlPath,
|
|
211
|
+
message: "Wireframe output is not valid HTML content.",
|
|
212
|
+
suggestion: "Ensure wireframe companion HTML contains a valid document structure."
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const htmlFiles = await listHtmlFiles(path.dirname(artifact.file));
|
|
218
|
+
if (htmlFiles.length < 1) {
|
|
219
|
+
issues.push({
|
|
220
|
+
level: "error",
|
|
221
|
+
code: "wireframe_screens_missing",
|
|
222
|
+
check: "schema",
|
|
223
|
+
artifactType: artifact.type,
|
|
224
|
+
file: artifact.file,
|
|
225
|
+
message: "Wireframe must include at least one HTML screen artifact.",
|
|
226
|
+
suggestion: "Regenerate wireframe to create paired .md and .html screen files."
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
issues.push(...(await checkConsistency(cwd, loaded, normalizedBrief)));
|
|
233
|
+
if (options.strict) {
|
|
234
|
+
for (const issue of issues) {
|
|
235
|
+
if (issue.level === "warning") issue.level = "error";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const finalReportPath = options.report ? path.resolve(cwd, options.report) : reportPath(cwd);
|
|
240
|
+
if (!isPathInside(path.join(cwd, "product-docs"), finalReportPath)) {
|
|
241
|
+
throw new UserError("Validation report must be inside `product-docs/`.");
|
|
242
|
+
}
|
|
243
|
+
await writeReport(finalReportPath, issues);
|
|
244
|
+
const pass = !issues.some((issue) => issue.level === "error");
|
|
245
|
+
return { pass, reportPath: finalReportPath, issues };
|
|
246
|
+
}
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import Ajv2020 from "ajv/dist/2020";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { sectionTextMap } from "./markdown";
|
|
6
|
+
import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
|
|
7
|
+
import { schemaPath } from "./paths";
|
|
8
|
+
|
|
9
|
+
type SchemaWithHeading = Record<string, unknown> & {
|
|
10
|
+
x_required_headings?: unknown;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
14
|
+
addFormats(ajv);
|
|
15
|
+
|
|
16
|
+
async function resolveSchema(
|
|
17
|
+
cwd: string,
|
|
18
|
+
artifactType: ArtifactType
|
|
19
|
+
): Promise<SchemaWithHeading> {
|
|
20
|
+
const raw = await fs.readFile(schemaPath(cwd, artifactType), "utf8");
|
|
21
|
+
return yaml.load(raw) as SchemaWithHeading;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function requiredHeadingsFromSchema(schema: SchemaWithHeading): string[] {
|
|
25
|
+
if (!Array.isArray(schema.x_required_headings)) return [];
|
|
26
|
+
return schema.x_required_headings.filter((item): item is string => typeof item === "string");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function validateSchema(
|
|
30
|
+
cwd: string,
|
|
31
|
+
artifactType: ArtifactType,
|
|
32
|
+
doc: ArtifactDoc,
|
|
33
|
+
requiredHeadingsOverride?: string[]
|
|
34
|
+
): Promise<{ issues: ValidationIssue[]; requiredHeadings: string[] }> {
|
|
35
|
+
const schema = await resolveSchema(cwd, artifactType);
|
|
36
|
+
const requiredHeadings =
|
|
37
|
+
requiredHeadingsOverride && requiredHeadingsOverride.length > 0
|
|
38
|
+
? requiredHeadingsOverride
|
|
39
|
+
: requiredHeadingsFromSchema(schema);
|
|
40
|
+
const workingSchema: Record<string, unknown> = { ...schema };
|
|
41
|
+
delete workingSchema.x_required_headings;
|
|
42
|
+
|
|
43
|
+
const validate = ajv.compile(workingSchema);
|
|
44
|
+
const valid = validate(doc);
|
|
45
|
+
const issues: ValidationIssue[] = [];
|
|
46
|
+
|
|
47
|
+
if (!valid && validate.errors) {
|
|
48
|
+
for (const err of validate.errors) {
|
|
49
|
+
issues.push({
|
|
50
|
+
level: "error",
|
|
51
|
+
code: "schema_validation_failed",
|
|
52
|
+
check: "schema",
|
|
53
|
+
artifactType,
|
|
54
|
+
field: err.instancePath || err.schemaPath,
|
|
55
|
+
message: `Schema validation error: ${err.message ?? "unknown error"}`,
|
|
56
|
+
suggestion: "Adjust the generated content to satisfy schema requirements."
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const sections = sectionTextMap(doc.body);
|
|
62
|
+
const trMode = String((doc.frontmatter as Record<string, unknown>).language ?? "").toLowerCase().startsWith("tr");
|
|
63
|
+
if (trMode) {
|
|
64
|
+
return { issues, requiredHeadings: [] };
|
|
65
|
+
}
|
|
66
|
+
for (const heading of requiredHeadings) {
|
|
67
|
+
if (!doc.body.includes(heading)) {
|
|
68
|
+
issues.push({
|
|
69
|
+
level: "error",
|
|
70
|
+
code: "missing_required_heading",
|
|
71
|
+
check: "schema",
|
|
72
|
+
artifactType,
|
|
73
|
+
field: heading,
|
|
74
|
+
message: `Required section missing: ${heading}`,
|
|
75
|
+
suggestion: "Regenerate or manually edit the artifact to include all required headings."
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const content = sections.get(heading) ?? "";
|
|
81
|
+
const isPlaceholder = /(tbd|to be defined|i don't know|unknown|n\/a)/i.test(content);
|
|
82
|
+
if (content.trim().length < 20 || isPlaceholder) {
|
|
83
|
+
issues.push({
|
|
84
|
+
level: "error",
|
|
85
|
+
code: "weak_required_heading_content",
|
|
86
|
+
check: "schema",
|
|
87
|
+
artifactType,
|
|
88
|
+
field: heading,
|
|
89
|
+
message: `Section has weak or placeholder content: ${heading}`,
|
|
90
|
+
suggestion: "Replace placeholders with concrete, actionable details."
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { issues, requiredHeadings };
|
|
96
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileExists } from "./utils";
|
|
4
|
+
|
|
5
|
+
export async function readCliVersion(cwd: string): Promise<string> {
|
|
6
|
+
const candidates = [
|
|
7
|
+
path.join(cwd, "package.json"),
|
|
8
|
+
path.resolve(__dirname, "..", "package.json")
|
|
9
|
+
];
|
|
10
|
+
for (const candidate of candidates) {
|
|
11
|
+
if (!(await fileExists(candidate))) continue;
|
|
12
|
+
try {
|
|
13
|
+
const raw = await fs.readFile(candidate, "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw) as { version?: unknown };
|
|
15
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
|
|
16
|
+
return parsed.version.trim();
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// ignore and continue
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return "0.0.0";
|
|
23
|
+
}
|
|
24
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type WorkflowCommand = {
|
|
2
|
+
name: string;
|
|
3
|
+
cliSubcommand: string;
|
|
4
|
+
description: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
const BASE_WORKFLOW_COMMANDS: WorkflowCommand[] = [
|
|
8
|
+
{ name: "prodo-normalize", cliSubcommand: "normalize", description: "Normalize start brief into normalized brief JSON." },
|
|
9
|
+
{ name: "prodo-prd", cliSubcommand: "prd", description: "Generate PRD artifact from normalized brief." },
|
|
10
|
+
{ name: "prodo-workflow", cliSubcommand: "workflow", description: "Generate workflow artifact." },
|
|
11
|
+
{ name: "prodo-wireframe", cliSubcommand: "wireframe", description: "Generate wireframe artifact." },
|
|
12
|
+
{ name: "prodo-stories", cliSubcommand: "stories", description: "Generate stories artifact." },
|
|
13
|
+
{ name: "prodo-techspec", cliSubcommand: "techspec", description: "Generate technical specification artifact." },
|
|
14
|
+
{ name: "prodo-validate", cliSubcommand: "validate", description: "Run schema and cross-artifact consistency validation." }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export const WORKFLOW_COMMANDS: WorkflowCommand[] = BASE_WORKFLOW_COMMANDS;
|
|
18
|
+
|
|
19
|
+
export function buildWorkflowCommands(artifactTypes: string[]): WorkflowCommand[] {
|
|
20
|
+
const commandByName = new Map<string, WorkflowCommand>(BASE_WORKFLOW_COMMANDS.map((item) => [item.name, item]));
|
|
21
|
+
for (const type of artifactTypes) {
|
|
22
|
+
const name = `prodo-${type}`;
|
|
23
|
+
if (commandByName.has(name)) continue;
|
|
24
|
+
commandByName.set(name, {
|
|
25
|
+
name,
|
|
26
|
+
cliSubcommand: type,
|
|
27
|
+
description: `Generate ${type} artifact from normalized brief.`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return Array.from(commandByName.values());
|
|
31
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Product Requirements Document (PRD)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Document Control
|
|
6
|
+
|
|
7
|
+
| Version | Date | Author | Description |
|
|
8
|
+
|--------|------|--------|-------------|
|
|
9
|
+
| v1.0 | {{date}} | {{author}} | Initial version |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2. Product Overview
|
|
14
|
+
|
|
15
|
+
### 2.1 Product Summary
|
|
16
|
+
|
|
17
|
+
{{Provide a concise description of the product, what it does, and its core value proposition.}}
|
|
18
|
+
|
|
19
|
+
### 2.2 Problem Statement
|
|
20
|
+
|
|
21
|
+
{{Describe the core problem being solved. Who is affected and why it matters.}}
|
|
22
|
+
|
|
23
|
+
### 2.3 Goals & Objectives
|
|
24
|
+
|
|
25
|
+
- **G1:** {{Primary business/product goal}}
|
|
26
|
+
- **G2:** {{Secondary goal}}
|
|
27
|
+
- **G3:** {{Optional}}
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 3. Functional Requirements
|
|
32
|
+
|
|
33
|
+
### 3.1 Core System Logic
|
|
34
|
+
|
|
35
|
+
#### Primary Features
|
|
36
|
+
|
|
37
|
+
- **FR-XX-01:** {{System must...}}
|
|
38
|
+
- **FR-XX-02:** {{System must...}}
|
|
39
|
+
- **FR-XX-03:** {{System must...}}
|
|
40
|
+
|
|
41
|
+
#### Advanced Rules / Variations
|
|
42
|
+
|
|
43
|
+
- **FR-XX-04:** {{Conditional logic / variations}}
|
|
44
|
+
- **FR-XX-05:** {{Behavior-based rules}}
|
|
45
|
+
- **FR-XX-06:** {{Special cases}}
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### 3.2 Constraints & Limits
|
|
50
|
+
|
|
51
|
+
- **FR-XX-07:** {{Max limits}}
|
|
52
|
+
- **FR-XX-08:** {{Time-based limits}}
|
|
53
|
+
- **FR-XX-09:** {{Usage restrictions}}
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
### 3.3 Wallet / Balance / State Management (if applicable)
|
|
58
|
+
|
|
59
|
+
- **FR-STATE-01:** {{State update logic}}
|
|
60
|
+
- **FR-STATE-02:** {{User visibility}}
|
|
61
|
+
- **FR-STATE-03:** {{Separation of balances/categories}}
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### 3.4 Exception & Recovery Handling
|
|
66
|
+
|
|
67
|
+
- **FR-ERR-01:** {{Full rollback / reversal logic}}
|
|
68
|
+
- **FR-ERR-02:** {{Partial correction logic}}
|
|
69
|
+
- **FR-ERR-03:** {{Negative state prevention}}
|
|
70
|
+
- **FR-ERR-04:** {{User notification for corrections}}
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### 3.5 Notifications
|
|
75
|
+
|
|
76
|
+
- **FR-NOTIF-01:** {{Real-time notification}}
|
|
77
|
+
- **FR-NOTIF-02:** {{Special case notification}}
|
|
78
|
+
- **FR-NOTIF-03:** {{Periodic summary}}
|
|
79
|
+
- **FR-NOTIF-04:** {{Threshold/limit warnings}}
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### 3.6 Admin / Backoffice
|
|
84
|
+
|
|
85
|
+
- **FR-ADMIN-01:** {{Rule management}}
|
|
86
|
+
- **FR-ADMIN-02:** {{Configuration control}}
|
|
87
|
+
- **FR-ADMIN-03:** {{Fraud/manual intervention}}
|
|
88
|
+
- **FR-ADMIN-04:** {{Audit & monitoring}}
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 4. Non-Functional Requirements
|
|
93
|
+
|
|
94
|
+
### 4.1 Performance
|
|
95
|
+
|
|
96
|
+
- **NFR-PERF-01:** {{Latency requirements}}
|
|
97
|
+
- **NFR-PERF-02:** {{Throughput requirements}}
|
|
98
|
+
- **NFR-PERF-03:** {{API performance}}
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### 4.2 Security
|
|
103
|
+
|
|
104
|
+
- **NFR-SEC-01:** {{Fraud detection / abuse prevention}}
|
|
105
|
+
- **NFR-SEC-02:** {{Auditability}}
|
|
106
|
+
- **NFR-SEC-03:** {{Encryption / data protection}}
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 4.3 Reliability
|
|
111
|
+
|
|
112
|
+
- **NFR-REL-01:** {{Uptime}}
|
|
113
|
+
- **NFR-REL-02:** {{Failure handling}}
|
|
114
|
+
- **NFR-REL-03:** {{Graceful degradation}}
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 4.4 Scalability
|
|
119
|
+
|
|
120
|
+
- **NFR-SCAL-01:** {{Horizontal scalability}}
|
|
121
|
+
- **NFR-SCAL-02:** {{Traffic spikes}}
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 5. Data Requirements
|
|
126
|
+
|
|
127
|
+
### 5.1 Data Entities
|
|
128
|
+
|
|
129
|
+
- {{Entity 1}}
|
|
130
|
+
- {{Entity 2}}
|
|
131
|
+
- {{Entity 3}}
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### 5.2 Data Retention
|
|
136
|
+
|
|
137
|
+
- {{Retention rule 1}}
|
|
138
|
+
- {{Retention rule 2}}
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 6. Integrations & Constraints
|
|
143
|
+
|
|
144
|
+
### 6.1 External Integrations
|
|
145
|
+
|
|
146
|
+
- {{Integration 1}}
|
|
147
|
+
- {{Integration 2}}
|
|
148
|
+
- {{Integration 3}}
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 6.2 Technical Constraints
|
|
153
|
+
|
|
154
|
+
- {{Platform constraints}}
|
|
155
|
+
- {{Database constraints}}
|
|
156
|
+
- {{Compliance constraints}}
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### 6.3 Performance Targets
|
|
161
|
+
|
|
162
|
+
| Metric | Target | Measurement |
|
|
163
|
+
|--------|--------|-------------|
|
|
164
|
+
| {{metric}} | {{target}} | {{method}} |
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 7. User Flows
|
|
169
|
+
|
|
170
|
+
### Flow 1: {{Flow Name}}
|
|
171
|
+
|
|
172
|
+
1. {{Step}}
|
|
173
|
+
2. {{Step}}
|
|
174
|
+
3. {{Step}}
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Flow 2: {{Flow Name}}
|
|
179
|
+
|
|
180
|
+
1. {{Step}}
|
|
181
|
+
2. {{Step}}
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 8. Compliance & Data Protection (if applicable)
|
|
186
|
+
|
|
187
|
+
### 8.1 Consent Mechanism
|
|
188
|
+
|
|
189
|
+
{{Describe user consent flow}}
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### 8.2 Legal Notes
|
|
194
|
+
|
|
195
|
+
{{Regulatory requirements and compliance context}}
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 9. Success Metrics
|
|
200
|
+
|
|
201
|
+
### 9.1 Product KPIs
|
|
202
|
+
|
|
203
|
+
- {{KPI 1}}
|
|
204
|
+
- {{KPI 2}}
|
|
205
|
+
- {{KPI 3}}
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### 9.2 Operational Metrics
|
|
210
|
+
|
|
211
|
+
- {{Metric 1}}
|
|
212
|
+
- {{Metric 2}}
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Notes
|
|
217
|
+
|
|
218
|
+
- This document defines product requirements only
|
|
219
|
+
- Technical implementation must be defined in a separate TechSpec
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# User Stories & Acceptance Criteria
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Legend
|
|
6
|
+
|
|
7
|
+
- **Priority:** P0 (Critical), P1 (High), P2 (Medium), P3 (Low)
|
|
8
|
+
- **Estimate:** Story Points (Fibonacci: 1, 2, 3, 5, 8, 13)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 2. Epic Overview
|
|
13
|
+
|
|
14
|
+
- **EPIC-01:** {{Epic Name}}
|
|
15
|
+
- **EPIC-02:** {{Epic Name}}
|
|
16
|
+
- **EPIC-03:** {{Epic Name}}
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3. Detailed User Stories
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### EPIC-XX: {{Epic Name}}
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
#### US-XX: {{Short Title}}
|
|
29
|
+
|
|
30
|
+
**As a** {{user type}},
|
|
31
|
+
**I want** {{desired action}},
|
|
32
|
+
**So that** {{expected outcome/value}}.
|
|
33
|
+
|
|
34
|
+
- **Priority:** P0 | P1 | P2 | P3
|
|
35
|
+
- **Estimate:** {{story points}} SP
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
### Acceptance Criteria
|
|
40
|
+
|
|
41
|
+
```gherkin
|
|
42
|
+
Scenario: {{Scenario name}}
|
|
43
|
+
|
|
44
|
+
Given {{initial state}}
|
|
45
|
+
When {{action happens}}
|
|
46
|
+
Then {{expected result}}
|
|
47
|
+
|
|
48
|
+
And {{additional condition}}
|
|
49
|
+
And {{additional validation}}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Technical Specification (TechSpec)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Document Control
|
|
6
|
+
|
|
7
|
+
| Version | Date | Author | Description |
|
|
8
|
+
|--------|------|--------|-------------|
|
|
9
|
+
| v1.0 | {{date}} | {{author}} | Initial version |
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2. Overview
|
|
14
|
+
|
|
15
|
+
### 2.1 Purpose
|
|
16
|
+
|
|
17
|
+
{{Describe what this system/component does in technical terms}}
|
|
18
|
+
|
|
19
|
+
### 2.2 Scope
|
|
20
|
+
|
|
21
|
+
{{Define boundaries of the system. What is included / excluded}}
|
|
22
|
+
|
|
23
|
+
### 2.3 Related Documents
|
|
24
|
+
|
|
25
|
+
- PRD: {{link}}
|
|
26
|
+
- User Stories: {{link}}
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 3. System Architecture
|
|
31
|
+
|
|
32
|
+
### 3.1 High-Level Architecture
|
|
33
|
+
|
|
34
|
+
{{Describe system components and interactions}}
|
|
35
|
+
|
|
36
|
+
Optional (Mermaid):
|
|
37
|
+
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart TD
|
|
40
|
+
A[Client] --> B[API Gateway]
|
|
41
|
+
B --> C[Service Layer]
|
|
42
|
+
C --> D[Database]
|