@shahmarasy/prodo 0.1.4 → 0.1.6

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 (205) 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 +467 -0
  27. package/dist/cli/init-tui.d.ts +23 -0
  28. package/dist/cli/init-tui.js +183 -0
  29. package/dist/cli/init.d.ts +12 -0
  30. package/dist/cli/init.js +335 -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 +8 -0
  66. package/dist/core/settings.js +43 -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 +12 -11
  97. package/dist/providers/openai-provider.d.ts +1 -1
  98. package/dist/providers/openai-provider.js +13 -13
  99. package/dist/skill-engine/context.d.ts +7 -0
  100. package/dist/skill-engine/context.js +76 -0
  101. package/dist/skill-engine/discovery.d.ts +2 -0
  102. package/dist/skill-engine/discovery.js +52 -0
  103. package/dist/skill-engine/graph.d.ts +4 -0
  104. package/dist/skill-engine/graph.js +114 -0
  105. package/dist/skill-engine/index.d.ts +11 -0
  106. package/dist/skill-engine/index.js +49 -0
  107. package/dist/skill-engine/pipeline.d.ts +9 -0
  108. package/dist/skill-engine/pipeline.js +84 -0
  109. package/dist/skill-engine/registry.d.ts +12 -0
  110. package/dist/skill-engine/registry.js +74 -0
  111. package/dist/skill-engine/types.d.ts +66 -0
  112. package/dist/skill-engine/types.js +2 -0
  113. package/dist/skill-engine/validator.d.ts +4 -0
  114. package/dist/skill-engine/validator.js +90 -0
  115. package/dist/skills/engine.d.ts +10 -0
  116. package/dist/skills/engine.js +75 -0
  117. package/dist/skills/fix-skill.d.ts +2 -0
  118. package/dist/skills/fix-skill.js +38 -0
  119. package/dist/skills/fix.d.ts +2 -0
  120. package/dist/skills/fix.js +41 -0
  121. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  122. package/dist/skills/generate-artifact-skill.js +32 -0
  123. package/dist/skills/generate-artifact.d.ts +2 -0
  124. package/dist/skills/generate-artifact.js +42 -0
  125. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  126. package/dist/skills/generate-pipeline-skill.js +45 -0
  127. package/dist/skills/normalize-skill.d.ts +2 -0
  128. package/dist/skills/normalize-skill.js +29 -0
  129. package/dist/skills/normalize.d.ts +2 -0
  130. package/dist/skills/normalize.js +29 -0
  131. package/dist/skills/register-core.d.ts +2 -0
  132. package/dist/skills/register-core.js +21 -0
  133. package/dist/skills/types.d.ts +28 -0
  134. package/dist/skills/types.js +2 -0
  135. package/dist/skills/validate-skill.d.ts +2 -0
  136. package/dist/skills/validate-skill.js +29 -0
  137. package/dist/skills/validate.d.ts +2 -0
  138. package/dist/skills/validate.js +37 -0
  139. package/package.json +72 -45
  140. package/src/agents/agent-registry.ts +93 -0
  141. package/src/agents/anthropic/index.ts +86 -0
  142. package/src/agents/anthropic/manifest.json +7 -0
  143. package/src/agents/base.ts +77 -0
  144. package/src/agents/google/index.ts +79 -0
  145. package/src/agents/google/manifest.json +7 -0
  146. package/src/agents/mock/index.ts +32 -0
  147. package/src/agents/mock/manifest.json +7 -0
  148. package/src/agents/openai/index.ts +83 -0
  149. package/src/agents/openai/manifest.json +7 -0
  150. package/src/agents/system-prompts.ts +35 -0
  151. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  152. package/src/{agents.ts → cli/agent-ids.ts} +58 -58
  153. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  154. package/src/cli/fix-tui.ts +111 -0
  155. package/src/{cli.ts → cli/index.ts} +463 -410
  156. package/src/{init-tui.ts → cli/init-tui.ts} +49 -37
  157. package/src/{init.ts → cli/init.ts} +399 -398
  158. package/src/cli/normalize-interactive.ts +241 -0
  159. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  160. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  161. package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
  162. package/src/core/clean.ts +88 -0
  163. package/src/{consistency.ts → core/consistency.ts} +374 -303
  164. package/src/{constants.ts → core/constants.ts} +72 -72
  165. package/src/{errors.ts → core/errors.ts} +7 -7
  166. package/src/core/fix.ts +253 -0
  167. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  168. package/src/{markdown.ts → core/markdown.ts} +93 -73
  169. package/src/{normalize.ts → core/normalize.ts} +145 -137
  170. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  171. package/src/{output-index.ts → core/output-index.ts} +59 -59
  172. package/src/{paths.ts → core/paths.ts} +75 -71
  173. package/src/{project-config.ts → core/project-config.ts} +78 -78
  174. package/src/{registry.ts → core/registry.ts} +119 -119
  175. package/src/{settings.ts → core/settings.ts} +8 -2
  176. package/src/core/template-engine.ts +45 -0
  177. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  178. package/src/{templates.ts → core/templates.ts} +452 -452
  179. package/src/core/terminology.ts +177 -0
  180. package/src/core/tracing.ts +110 -0
  181. package/src/{types.ts → core/types.ts} +46 -46
  182. package/src/{utils.ts → core/utils.ts} +64 -64
  183. package/src/{validate.ts → core/validate.ts} +252 -246
  184. package/src/{validator.ts → core/validator.ts} +92 -92
  185. package/src/{version.ts → core/version.ts} +24 -24
  186. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
  187. package/src/i18n/en.json +45 -0
  188. package/src/i18n/index.ts +58 -0
  189. package/src/i18n/tr.json +45 -0
  190. package/src/providers/index.ts +29 -12
  191. package/src/providers/mock-provider.ts +200 -199
  192. package/src/providers/openai-provider.ts +88 -88
  193. package/src/skill-engine/context.ts +90 -0
  194. package/src/skill-engine/discovery.ts +57 -0
  195. package/src/skill-engine/graph.ts +136 -0
  196. package/src/skill-engine/index.ts +55 -0
  197. package/src/skill-engine/pipeline.ts +112 -0
  198. package/src/skill-engine/registry.ts +75 -0
  199. package/src/skill-engine/types.ts +81 -0
  200. package/src/skill-engine/validator.ts +135 -0
  201. package/src/skills/fix.ts +45 -0
  202. package/src/skills/generate-artifact.ts +48 -0
  203. package/src/skills/normalize.ts +32 -0
  204. package/src/skills/register-core.ts +27 -0
  205. package/src/skills/validate.ts +40 -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
+ }