@shahmarasy/prodo 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +201 -97
  2. package/bin/prodo.cjs +6 -6
  3. package/dist/agents/agent-registry.d.ts +13 -0
  4. package/dist/agents/agent-registry.js +79 -0
  5. package/dist/agents/anthropic/index.d.ts +9 -0
  6. package/dist/agents/anthropic/index.js +55 -0
  7. package/dist/agents/base.d.ts +25 -0
  8. package/dist/agents/base.js +71 -0
  9. package/dist/agents/google/index.d.ts +9 -0
  10. package/dist/agents/google/index.js +53 -0
  11. package/dist/agents/mock/index.d.ts +11 -0
  12. package/dist/agents/mock/index.js +26 -0
  13. package/dist/agents/openai/index.d.ts +9 -0
  14. package/dist/agents/openai/index.js +57 -0
  15. package/dist/agents/system-prompts.d.ts +3 -0
  16. package/dist/agents/system-prompts.js +32 -0
  17. package/dist/cli/agent-command-installer.d.ts +4 -0
  18. package/dist/cli/agent-command-installer.js +148 -0
  19. package/dist/cli/agent-ids.d.ts +15 -0
  20. package/dist/cli/agent-ids.js +49 -0
  21. package/dist/cli/doctor.d.ts +1 -0
  22. package/dist/cli/doctor.js +144 -0
  23. package/dist/cli/fix-tui.d.ts +4 -0
  24. package/dist/cli/fix-tui.js +79 -0
  25. package/dist/cli/index.d.ts +9 -0
  26. package/dist/cli/index.js +465 -0
  27. package/dist/cli/init-tui.d.ts +23 -0
  28. package/dist/cli/init-tui.js +176 -0
  29. package/dist/cli/init.d.ts +11 -0
  30. package/dist/cli/init.js +334 -0
  31. package/dist/cli/normalize-interactive.d.ts +8 -0
  32. package/dist/cli/normalize-interactive.js +167 -0
  33. package/dist/cli/preset-loader.d.ts +4 -0
  34. package/dist/cli/preset-loader.js +210 -0
  35. package/dist/core/artifact-registry.d.ts +11 -0
  36. package/dist/core/artifact-registry.js +49 -0
  37. package/dist/core/artifacts.d.ts +10 -0
  38. package/dist/core/artifacts.js +892 -0
  39. package/dist/core/clean.d.ts +10 -0
  40. package/dist/core/clean.js +74 -0
  41. package/dist/core/consistency.d.ts +8 -0
  42. package/dist/core/consistency.js +328 -0
  43. package/dist/core/constants.d.ts +7 -0
  44. package/dist/core/constants.js +64 -0
  45. package/dist/core/errors.d.ts +3 -0
  46. package/dist/core/errors.js +10 -0
  47. package/dist/core/fix.d.ts +31 -0
  48. package/dist/core/fix.js +188 -0
  49. package/dist/core/hook-executor.d.ts +1 -0
  50. package/dist/core/hook-executor.js +175 -0
  51. package/dist/core/markdown.d.ts +16 -0
  52. package/dist/core/markdown.js +81 -0
  53. package/dist/core/normalize.d.ts +8 -0
  54. package/dist/core/normalize.js +125 -0
  55. package/dist/core/normalized-brief.d.ts +48 -0
  56. package/dist/core/normalized-brief.js +182 -0
  57. package/dist/core/output-index.d.ts +13 -0
  58. package/dist/core/output-index.js +55 -0
  59. package/dist/core/paths.d.ts +17 -0
  60. package/dist/core/paths.js +80 -0
  61. package/dist/core/project-config.d.ts +14 -0
  62. package/dist/core/project-config.js +69 -0
  63. package/dist/core/registry.d.ts +13 -0
  64. package/dist/core/registry.js +115 -0
  65. package/dist/core/settings.d.ts +7 -0
  66. package/dist/core/settings.js +35 -0
  67. package/dist/core/template-engine.d.ts +3 -0
  68. package/dist/core/template-engine.js +43 -0
  69. package/dist/core/template-resolver.d.ts +15 -0
  70. package/dist/core/template-resolver.js +46 -0
  71. package/dist/core/templates.d.ts +33 -0
  72. package/dist/core/templates.js +440 -0
  73. package/dist/core/terminology.d.ts +21 -0
  74. package/dist/core/terminology.js +143 -0
  75. package/dist/core/tracing.d.ts +21 -0
  76. package/dist/core/tracing.js +74 -0
  77. package/dist/core/types.d.ts +35 -0
  78. package/dist/core/types.js +5 -0
  79. package/dist/core/utils.d.ts +7 -0
  80. package/dist/core/utils.js +66 -0
  81. package/dist/core/validate.d.ts +10 -0
  82. package/dist/core/validate.js +226 -0
  83. package/dist/core/validator.d.ts +5 -0
  84. package/dist/core/validator.js +76 -0
  85. package/dist/core/version.d.ts +1 -0
  86. package/dist/core/version.js +30 -0
  87. package/dist/core/workflow-commands.d.ts +7 -0
  88. package/dist/core/workflow-commands.js +29 -0
  89. package/dist/i18n/en.json +45 -0
  90. package/dist/i18n/index.d.ts +5 -0
  91. package/dist/i18n/index.js +63 -0
  92. package/dist/i18n/tr.json +45 -0
  93. package/dist/providers/index.d.ts +2 -1
  94. package/dist/providers/index.js +20 -6
  95. package/dist/providers/mock-provider.d.ts +1 -1
  96. package/dist/providers/mock-provider.js +7 -6
  97. package/dist/providers/openai-provider.d.ts +1 -1
  98. package/dist/providers/openai-provider.js +1 -1
  99. package/dist/skills/engine.d.ts +10 -0
  100. package/dist/skills/engine.js +75 -0
  101. package/dist/skills/fix-skill.d.ts +2 -0
  102. package/dist/skills/fix-skill.js +38 -0
  103. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  104. package/dist/skills/generate-artifact-skill.js +32 -0
  105. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  106. package/dist/skills/generate-pipeline-skill.js +45 -0
  107. package/dist/skills/normalize-skill.d.ts +2 -0
  108. package/dist/skills/normalize-skill.js +29 -0
  109. package/dist/skills/types.d.ts +28 -0
  110. package/dist/skills/types.js +2 -0
  111. package/dist/skills/validate-skill.d.ts +2 -0
  112. package/dist/skills/validate-skill.js +29 -0
  113. package/package.json +74 -45
  114. package/src/agents/agent-registry.ts +93 -0
  115. package/src/agents/anthropic/index.ts +86 -0
  116. package/src/agents/anthropic/manifest.json +7 -0
  117. package/src/agents/base.ts +77 -0
  118. package/src/agents/google/index.ts +79 -0
  119. package/src/agents/google/manifest.json +7 -0
  120. package/src/agents/mock/index.ts +32 -0
  121. package/src/agents/mock/manifest.json +7 -0
  122. package/src/agents/openai/index.ts +83 -0
  123. package/src/agents/openai/manifest.json +7 -0
  124. package/src/agents/system-prompts.ts +35 -0
  125. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  126. package/src/{agents.ts → cli/agent-ids.ts} +58 -58
  127. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  128. package/src/cli/fix-tui.ts +111 -0
  129. package/src/{cli.ts → cli/index.ts} +459 -410
  130. package/src/{init-tui.ts → cli/init-tui.ts} +208 -208
  131. package/src/{init.ts → cli/init.ts} +398 -398
  132. package/src/cli/normalize-interactive.ts +241 -0
  133. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  134. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  135. package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
  136. package/src/core/clean.ts +88 -0
  137. package/src/{consistency.ts → core/consistency.ts} +374 -303
  138. package/src/{constants.ts → core/constants.ts} +72 -72
  139. package/src/{errors.ts → core/errors.ts} +7 -7
  140. package/src/core/fix.ts +253 -0
  141. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  142. package/src/{markdown.ts → core/markdown.ts} +93 -73
  143. package/src/{normalize.ts → core/normalize.ts} +145 -137
  144. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  145. package/src/{output-index.ts → core/output-index.ts} +59 -59
  146. package/src/{paths.ts → core/paths.ts} +75 -71
  147. package/src/{project-config.ts → core/project-config.ts} +78 -78
  148. package/src/{registry.ts → core/registry.ts} +119 -119
  149. package/src/{settings.ts → core/settings.ts} +35 -35
  150. package/src/core/template-engine.ts +45 -0
  151. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  152. package/src/{templates.ts → core/templates.ts} +452 -452
  153. package/src/core/terminology.ts +177 -0
  154. package/src/core/tracing.ts +110 -0
  155. package/src/{types.ts → core/types.ts} +46 -46
  156. package/src/{utils.ts → core/utils.ts} +64 -64
  157. package/src/{validate.ts → core/validate.ts} +252 -246
  158. package/src/{validator.ts → core/validator.ts} +92 -92
  159. package/src/{version.ts → core/version.ts} +24 -24
  160. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
  161. package/src/i18n/en.json +45 -0
  162. package/src/i18n/index.ts +58 -0
  163. package/src/i18n/tr.json +45 -0
  164. package/src/providers/index.ts +29 -12
  165. package/src/providers/mock-provider.ts +200 -199
  166. package/src/providers/openai-provider.ts +88 -88
  167. package/src/skills/engine.ts +94 -0
  168. package/src/skills/fix-skill.ts +38 -0
  169. package/src/skills/generate-artifact-skill.ts +32 -0
  170. package/src/skills/generate-pipeline-skill.ts +49 -0
  171. package/src/skills/normalize-skill.ts +29 -0
  172. package/src/skills/types.ts +36 -0
  173. package/src/skills/validate-skill.ts +29 -0
@@ -1,246 +1,252 @@
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
- }
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 type ValidateResult = {
109
+ pass: boolean;
110
+ reportPath: string;
111
+ issues: ValidationIssue[];
112
+ };
113
+
114
+ export async function runValidate(
115
+ cwd: string,
116
+ options: { strict?: boolean; report?: string }
117
+ ): Promise<ValidateResult> {
118
+ const normalizedPath = normalizedBriefPath(cwd);
119
+ if (!(await fileExists(normalizedPath))) {
120
+ throw new UserError("Missing `.prodo/briefs/normalized-brief.json`. Run `prodo-init` and create it first.");
121
+ }
122
+
123
+ const normalizedBrief = await readJsonFile<Record<string, unknown>>(normalizedPath);
124
+ const loaded = await loadLatestArtifacts(cwd);
125
+ const issues: ValidationIssue[] = [];
126
+
127
+ for (const artifact of loaded) {
128
+ const template = await resolveTemplate({
129
+ cwd,
130
+ artifactType: artifact.type
131
+ });
132
+ const headings = template ? extractRequiredHeadingsFromTemplate(template.content) : [];
133
+ const schemaCheck = await validateSchema(
134
+ cwd,
135
+ artifact.type,
136
+ artifact.doc,
137
+ headings
138
+ );
139
+ issues.push(...schemaCheck.issues.map((issue) => ({ ...issue, file: artifact.file })));
140
+
141
+ if (artifact.type === "workflow") {
142
+ const ext = path.extname(artifact.file).toLowerCase();
143
+ if (ext !== ".md") {
144
+ issues.push({
145
+ level: "error",
146
+ code: "workflow_markdown_missing",
147
+ check: "schema",
148
+ artifactType: artifact.type,
149
+ file: artifact.file,
150
+ message: "Workflow explanation artifact must be Markdown (.md).",
151
+ suggestion: "Regenerate workflow so explanation is written to .md."
152
+ });
153
+ }
154
+ const mmdPath = path.join(path.dirname(artifact.file), `${path.parse(artifact.file).name}.mmd`);
155
+ if (!(await fileExists(mmdPath))) {
156
+ issues.push({
157
+ level: "error",
158
+ code: "workflow_mermaid_missing",
159
+ check: "schema",
160
+ artifactType: artifact.type,
161
+ file: artifact.file,
162
+ message: "Workflow Mermaid companion file (.mmd) is missing.",
163
+ suggestion: "Regenerate workflow so markdown and .mmd are produced as a pair."
164
+ });
165
+ } else {
166
+ const mmdRaw = await fs.readFile(mmdPath, "utf8");
167
+ const mermaidLike = /(^|\n)\s*flowchart\s+/i.test(mmdRaw) || /(^|\n)\s*graph\s+/i.test(mmdRaw);
168
+ if (!mermaidLike) {
169
+ issues.push({
170
+ level: "error",
171
+ code: "workflow_mermaid_invalid",
172
+ check: "schema",
173
+ artifactType: artifact.type,
174
+ file: mmdPath,
175
+ message: "Workflow Mermaid file is invalid or prose-only.",
176
+ suggestion: "Ensure .mmd file contains valid Mermaid diagram syntax."
177
+ });
178
+ }
179
+ }
180
+ }
181
+
182
+ if (artifact.type === "wireframe") {
183
+ const ext = path.extname(artifact.file).toLowerCase();
184
+ if (ext !== ".md") {
185
+ issues.push({
186
+ level: "error",
187
+ code: "wireframe_markdown_missing",
188
+ check: "schema",
189
+ artifactType: artifact.type,
190
+ file: artifact.file,
191
+ message: "Wireframe explanation artifact must be Markdown (.md).",
192
+ suggestion: "Regenerate wireframe so explanation is written to .md."
193
+ });
194
+ }
195
+
196
+ const htmlPath = path.join(path.dirname(artifact.file), `${path.parse(artifact.file).name}.html`);
197
+ if (!(await fileExists(htmlPath))) {
198
+ issues.push({
199
+ level: "error",
200
+ code: "wireframe_html_missing",
201
+ check: "schema",
202
+ artifactType: artifact.type,
203
+ file: artifact.file,
204
+ message: "Wireframe HTML companion file is missing.",
205
+ suggestion: "Regenerate wireframe so markdown and .html are produced as a pair."
206
+ });
207
+ } else {
208
+ const htmlRaw = await fs.readFile(htmlPath, "utf8");
209
+ const htmlLooksValid = /<!doctype html>/i.test(htmlRaw) || /<html[\s>]/i.test(htmlRaw);
210
+ if (!htmlLooksValid) {
211
+ issues.push({
212
+ level: "error",
213
+ code: "wireframe_html_invalid",
214
+ check: "schema",
215
+ artifactType: artifact.type,
216
+ file: htmlPath,
217
+ message: "Wireframe output is not valid HTML content.",
218
+ suggestion: "Ensure wireframe companion HTML contains a valid document structure."
219
+ });
220
+ }
221
+ }
222
+
223
+ const htmlFiles = await listHtmlFiles(path.dirname(artifact.file));
224
+ if (htmlFiles.length < 1) {
225
+ issues.push({
226
+ level: "error",
227
+ code: "wireframe_screens_missing",
228
+ check: "schema",
229
+ artifactType: artifact.type,
230
+ file: artifact.file,
231
+ message: "Wireframe must include at least one HTML screen artifact.",
232
+ suggestion: "Regenerate wireframe to create paired .md and .html screen files."
233
+ });
234
+ }
235
+ }
236
+ }
237
+
238
+ issues.push(...(await checkConsistency(cwd, loaded, normalizedBrief)));
239
+ if (options.strict) {
240
+ for (const issue of issues) {
241
+ if (issue.level === "warning") issue.level = "error";
242
+ }
243
+ }
244
+
245
+ const finalReportPath = options.report ? path.resolve(cwd, options.report) : reportPath(cwd);
246
+ if (!isPathInside(path.join(cwd, "product-docs"), finalReportPath)) {
247
+ throw new UserError("Validation report must be inside `product-docs/`.");
248
+ }
249
+ await writeReport(finalReportPath, issues);
250
+ const pass = !issues.some((issue) => issue.level === "error");
251
+ return { pass, reportPath: finalReportPath, issues };
252
+ }