@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
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTermMap = buildTermMap;
4
+ exports.checkTermReconciliation = checkTermReconciliation;
5
+ const markdown_1 = require("./markdown");
6
+ function normalizeTerm(term) {
7
+ return term
8
+ .toLowerCase()
9
+ .replace(/[^a-z0-9\u00c0-\u024f\s]/g, "")
10
+ .replace(/\s+/g, " ")
11
+ .trim();
12
+ }
13
+ function extractBoldTerms(body) {
14
+ const matches = body.match(/\*\*([^*]+)\*\*/g) ?? [];
15
+ return matches
16
+ .map((m) => m.replace(/\*\*/g, "").trim())
17
+ .filter((t) => t.length > 2 && t.length < 60);
18
+ }
19
+ function buildTermMap(normalizedBrief, loadedArtifacts) {
20
+ const termMap = new Map();
21
+ function addTerm(term, source, definition) {
22
+ const normalized = normalizeTerm(term);
23
+ if (!normalized || normalized.length < 2)
24
+ return;
25
+ const existing = termMap.get(normalized);
26
+ if (existing) {
27
+ existing.sources.push(source);
28
+ if (definition && !existing.definition) {
29
+ existing.definition = definition;
30
+ }
31
+ }
32
+ else {
33
+ termMap.set(normalized, {
34
+ term,
35
+ normalizedTerm: normalized,
36
+ definition,
37
+ sources: [source]
38
+ });
39
+ }
40
+ }
41
+ addTerm(normalizedBrief.product_name, { artifactType: "brief", file: "brief.md" });
42
+ for (const goal of normalizedBrief.goals) {
43
+ addTerm(goal, { artifactType: "brief", file: "brief.md" }, goal);
44
+ }
45
+ for (const feature of normalizedBrief.core_features) {
46
+ addTerm(feature, { artifactType: "brief", file: "brief.md" }, feature);
47
+ }
48
+ for (const constraint of normalizedBrief.constraints) {
49
+ addTerm(constraint, { artifactType: "brief", file: "brief.md" }, constraint);
50
+ }
51
+ for (const contract of normalizedBrief.contracts.goals) {
52
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
53
+ }
54
+ for (const contract of normalizedBrief.contracts.core_features) {
55
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
56
+ }
57
+ for (const contract of normalizedBrief.contracts.constraints) {
58
+ addTerm(contract.text, { artifactType: "brief", file: "brief.md" }, contract.text);
59
+ }
60
+ for (const artifact of loadedArtifacts) {
61
+ const sections = (0, markdown_1.parseMarkdownSections)(artifact.doc.body);
62
+ for (const section of sections) {
63
+ if (section.level === 2) {
64
+ addTerm(section.heading, {
65
+ artifactType: artifact.type,
66
+ file: artifact.file,
67
+ heading: section.heading
68
+ });
69
+ }
70
+ }
71
+ const boldTerms = extractBoldTerms(artifact.doc.body);
72
+ for (const boldTerm of boldTerms) {
73
+ addTerm(boldTerm, { artifactType: artifact.type, file: artifact.file });
74
+ }
75
+ }
76
+ return termMap;
77
+ }
78
+ function levenshtein(a, b) {
79
+ const m = a.length;
80
+ const n = b.length;
81
+ if (m === 0)
82
+ return n;
83
+ if (n === 0)
84
+ return m;
85
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
86
+ for (let i = 0; i <= m; i++)
87
+ dp[i][0] = i;
88
+ for (let j = 0; j <= n; j++)
89
+ dp[0][j] = j;
90
+ for (let i = 1; i <= m; i++) {
91
+ for (let j = 1; j <= n; j++) {
92
+ dp[i][j] = a[i - 1] === b[j - 1]
93
+ ? dp[i - 1][j - 1]
94
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
95
+ }
96
+ }
97
+ return dp[m][n];
98
+ }
99
+ function sharesStem(a, b) {
100
+ const wordsA = a.split(/\s+/).filter((w) => w.length > 3);
101
+ const wordsB = b.split(/\s+/).filter((w) => w.length > 3);
102
+ for (const wa of wordsA) {
103
+ for (const wb of wordsB) {
104
+ const shorter = Math.min(wa.length, wb.length);
105
+ const prefix = Math.min(4, shorter);
106
+ if (wa.slice(0, prefix) === wb.slice(0, prefix) && wa !== wb) {
107
+ return true;
108
+ }
109
+ }
110
+ }
111
+ return false;
112
+ }
113
+ function checkTermReconciliation(termMap) {
114
+ const issues = [];
115
+ const entries = Array.from(termMap.values()).filter((e) => e.sources.length > 0 && e.normalizedTerm.length > 3);
116
+ for (let i = 0; i < entries.length; i++) {
117
+ for (let j = i + 1; j < entries.length; j++) {
118
+ const a = entries[i];
119
+ const b = entries[j];
120
+ if (a.normalizedTerm === b.normalizedTerm)
121
+ continue;
122
+ const dist = levenshtein(a.normalizedTerm, b.normalizedTerm);
123
+ const maxLen = Math.max(a.normalizedTerm.length, b.normalizedTerm.length);
124
+ const ratio = dist / maxLen;
125
+ const isSimilar = (ratio < 0.25 && maxLen > 5) || sharesStem(a.normalizedTerm, b.normalizedTerm);
126
+ if (isSimilar) {
127
+ const aTypes = [...new Set(a.sources.map((s) => s.artifactType))];
128
+ const bTypes = [...new Set(b.sources.map((s) => s.artifactType))];
129
+ const crossDoc = aTypes.some((t) => !bTypes.includes(t)) || bTypes.some((t) => !aTypes.includes(t));
130
+ if (crossDoc) {
131
+ issues.push({
132
+ level: "warning",
133
+ code: "term_inconsistency",
134
+ check: "terminology",
135
+ message: `Similar terms used across documents: "${a.term}" (${aTypes.join(", ")}) vs "${b.term}" (${bTypes.join(", ")})`,
136
+ suggestion: "Standardize terminology across all artifacts to avoid ambiguity."
137
+ });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ return issues;
143
+ }
@@ -0,0 +1,21 @@
1
+ import { type NormalizedBrief } from "./normalized-brief";
2
+ import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
3
+ type LoadedArtifact = {
4
+ type: ArtifactType;
5
+ file: string;
6
+ doc: ArtifactDoc;
7
+ };
8
+ export type TraceEntry = {
9
+ contractId: string;
10
+ contractText: string;
11
+ category: "goals" | "core_features" | "constraints";
12
+ references: Array<{
13
+ artifactType: ArtifactType;
14
+ file: string;
15
+ line: string;
16
+ }>;
17
+ };
18
+ export type TraceMap = Map<string, TraceEntry>;
19
+ export declare function buildTraceMap(normalizedBrief: NormalizedBrief, loadedArtifacts: LoadedArtifact[]): TraceMap;
20
+ export declare function checkRequirementCompleteness(traceMap: TraceMap, normalizedBrief: NormalizedBrief, presentArtifactTypes: ArtifactType[]): ValidationIssue[];
21
+ export {};
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildTraceMap = buildTraceMap;
4
+ exports.checkRequirementCompleteness = checkRequirementCompleteness;
5
+ const markdown_1 = require("./markdown");
6
+ function buildTraceMap(normalizedBrief, loadedArtifacts) {
7
+ const traceMap = new Map();
8
+ function registerContracts(items, category) {
9
+ for (const item of items) {
10
+ traceMap.set(item.id, {
11
+ contractId: item.id,
12
+ contractText: item.text,
13
+ category,
14
+ references: []
15
+ });
16
+ }
17
+ }
18
+ registerContracts(normalizedBrief.contracts.goals, "goals");
19
+ registerContracts(normalizedBrief.contracts.core_features, "core_features");
20
+ registerContracts(normalizedBrief.contracts.constraints, "constraints");
21
+ for (const artifact of loadedArtifacts) {
22
+ const tagged = (0, markdown_1.taggedLinesByContract)(artifact.doc.body);
23
+ for (const { contractId, line } of tagged) {
24
+ const entry = traceMap.get(contractId);
25
+ if (entry) {
26
+ entry.references.push({
27
+ artifactType: artifact.type,
28
+ file: artifact.file,
29
+ line
30
+ });
31
+ }
32
+ }
33
+ }
34
+ return traceMap;
35
+ }
36
+ function checkRequirementCompleteness(traceMap, normalizedBrief, presentArtifactTypes) {
37
+ const issues = [];
38
+ if (presentArtifactTypes.length === 0)
39
+ return issues;
40
+ for (const [contractId, entry] of traceMap) {
41
+ if (entry.references.length === 0) {
42
+ issues.push({
43
+ level: "warning",
44
+ code: "untraced_requirement",
45
+ check: "tracing",
46
+ message: `Contract ${contractId} ("${truncate(entry.contractText, 60)}") is not referenced in any generated artifact.`,
47
+ suggestion: `Ensure [${contractId}] tag appears in at least one artifact body.`
48
+ });
49
+ }
50
+ }
51
+ const hasPrd = presentArtifactTypes.includes("prd");
52
+ if (hasPrd) {
53
+ for (const [contractId, entry] of traceMap) {
54
+ if (entry.category === "goals") {
55
+ const inPrd = entry.references.some((r) => r.artifactType === "prd");
56
+ if (!inPrd && entry.references.length > 0) {
57
+ issues.push({
58
+ level: "warning",
59
+ code: "goal_missing_in_prd",
60
+ check: "tracing",
61
+ message: `Goal ${contractId} appears in downstream artifacts but is not traced in the PRD.`,
62
+ suggestion: `Add [${contractId}] tag to the PRD to maintain traceability.`
63
+ });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return issues;
69
+ }
70
+ function truncate(text, maxLength) {
71
+ if (text.length <= maxLength)
72
+ return text;
73
+ return `${text.slice(0, maxLength - 3)}...`;
74
+ }
@@ -0,0 +1,35 @@
1
+ export declare const CORE_ARTIFACT_TYPES: readonly ["prd", "workflow", "wireframe", "stories", "techspec"];
2
+ export declare const ARTIFACT_TYPES: readonly ["prd", "workflow", "wireframe", "stories", "techspec"];
3
+ export type CoreArtifactType = (typeof CORE_ARTIFACT_TYPES)[number];
4
+ export type ArtifactType = CoreArtifactType | string;
5
+ export type ArtifactDoc = {
6
+ frontmatter: Record<string, unknown>;
7
+ body: string;
8
+ };
9
+ export type ContractCoverage = {
10
+ goals: string[];
11
+ core_features: string[];
12
+ constraints: string[];
13
+ };
14
+ export type ValidationIssue = {
15
+ level: "error" | "warning";
16
+ code: string;
17
+ check?: "schema" | "tag_coverage" | "semantic_consistency" | "contract_relevance" | "terminology" | "tracing" | "cross_reference";
18
+ artifactType?: ArtifactType;
19
+ file?: string;
20
+ field?: string;
21
+ message: string;
22
+ suggestion?: string;
23
+ };
24
+ export type GenerateResult = {
25
+ body: string;
26
+ frontmatter?: Record<string, unknown>;
27
+ };
28
+ export type ProviderSchemaHint = {
29
+ artifactType: ArtifactType | "normalize" | "semantic_consistency" | "contract_relevance";
30
+ requiredHeadings: string[];
31
+ requiredContracts: Array<keyof ContractCoverage>;
32
+ };
33
+ export type LLMProvider = {
34
+ generate: (prompt: string, inputContext: Record<string, unknown>, schemaHint: ProviderSchemaHint) => Promise<GenerateResult>;
35
+ };
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ARTIFACT_TYPES = exports.CORE_ARTIFACT_TYPES = void 0;
4
+ exports.CORE_ARTIFACT_TYPES = ["prd", "workflow", "wireframe", "stories", "techspec"];
5
+ exports.ARTIFACT_TYPES = exports.CORE_ARTIFACT_TYPES;
@@ -0,0 +1,7 @@
1
+ export declare function ensureDir(dirPath: string): Promise<void>;
2
+ export declare function fileExists(filePath: string): Promise<boolean>;
3
+ export declare function readJsonFile<T>(filePath: string): Promise<T>;
4
+ export declare function timestampSlug(date?: Date): string;
5
+ export declare function artifactFileStamp(date?: Date): string;
6
+ export declare function listFilesSortedByMtime(dirPath: string): Promise<string[]>;
7
+ export declare function isPathInside(parentDir: string, candidatePath: string): boolean;
@@ -0,0 +1,66 @@
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.ensureDir = ensureDir;
7
+ exports.fileExists = fileExists;
8
+ exports.readJsonFile = readJsonFile;
9
+ exports.timestampSlug = timestampSlug;
10
+ exports.artifactFileStamp = artifactFileStamp;
11
+ exports.listFilesSortedByMtime = listFilesSortedByMtime;
12
+ exports.isPathInside = isPathInside;
13
+ const promises_1 = __importDefault(require("node:fs/promises"));
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ async function ensureDir(dirPath) {
16
+ await promises_1.default.mkdir(dirPath, { recursive: true });
17
+ }
18
+ async function fileExists(filePath) {
19
+ try {
20
+ await promises_1.default.access(filePath);
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async function readJsonFile(filePath) {
28
+ const raw = await promises_1.default.readFile(filePath, "utf8");
29
+ return JSON.parse(raw);
30
+ }
31
+ function timestampSlug(date = new Date()) {
32
+ return date.toISOString().replace(/[:.]/g, "-");
33
+ }
34
+ function pad2(value) {
35
+ return String(value).padStart(2, "0");
36
+ }
37
+ function artifactFileStamp(date = new Date()) {
38
+ const year = date.getFullYear();
39
+ const month = pad2(date.getMonth() + 1);
40
+ const day = pad2(date.getDate());
41
+ const hour = pad2(date.getHours());
42
+ const minute = pad2(date.getMinutes());
43
+ const second = pad2(date.getSeconds());
44
+ return `${year}${month}${day}-${hour}${minute}${second}`;
45
+ }
46
+ async function listFilesSortedByMtime(dirPath) {
47
+ const exists = await fileExists(dirPath);
48
+ if (!exists)
49
+ return [];
50
+ const entries = await promises_1.default.readdir(dirPath);
51
+ const withStats = await Promise.all(entries.map(async (name) => {
52
+ const fullPath = node_path_1.default.join(dirPath, name);
53
+ const stat = await promises_1.default.stat(fullPath);
54
+ return { fullPath, mtimeMs: stat.mtimeMs, isFile: stat.isFile() };
55
+ }));
56
+ return withStats
57
+ .filter((entry) => entry.isFile)
58
+ .sort((a, b) => b.mtimeMs - a.mtimeMs)
59
+ .map((entry) => entry.fullPath);
60
+ }
61
+ function isPathInside(parentDir, candidatePath) {
62
+ const parent = node_path_1.default.resolve(parentDir);
63
+ const child = node_path_1.default.resolve(candidatePath);
64
+ const relative = node_path_1.default.relative(parent, child);
65
+ return relative === "" || (!relative.startsWith("..") && !node_path_1.default.isAbsolute(relative));
66
+ }
@@ -0,0 +1,10 @@
1
+ import type { ValidationIssue } from "./types";
2
+ export type ValidateResult = {
3
+ pass: boolean;
4
+ reportPath: string;
5
+ issues: ValidationIssue[];
6
+ };
7
+ export declare function runValidate(cwd: string, options: {
8
+ strict?: boolean;
9
+ report?: string;
10
+ }): Promise<ValidateResult>;
@@ -0,0 +1,226 @@
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.runValidate = runValidate;
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 artifact_registry_1 = require("./artifact-registry");
11
+ const consistency_1 = require("./consistency");
12
+ const errors_1 = require("./errors");
13
+ const output_index_1 = require("./output-index");
14
+ const paths_1 = require("./paths");
15
+ const template_resolver_1 = require("./template-resolver");
16
+ const utils_1 = require("./utils");
17
+ const validator_1 = require("./validator");
18
+ function sidecarPath(filePath) {
19
+ const parsed = node_path_1.default.parse(filePath);
20
+ return node_path_1.default.join(parsed.dir, `${parsed.name}.artifact.json`);
21
+ }
22
+ async function loadArtifactDoc(filePath) {
23
+ const sidecar = sidecarPath(filePath);
24
+ if (await (0, utils_1.fileExists)(sidecar)) {
25
+ const payload = await (0, utils_1.readJsonFile)(sidecar);
26
+ return {
27
+ frontmatter: payload.frontmatter ?? {},
28
+ body: typeof payload.body === "string" ? payload.body : ""
29
+ };
30
+ }
31
+ const raw = await promises_1.default.readFile(filePath, "utf8");
32
+ const parsed = (0, gray_matter_1.default)(raw);
33
+ return {
34
+ frontmatter: parsed.data,
35
+ body: parsed.content
36
+ };
37
+ }
38
+ async function loadLatestArtifacts(cwd) {
39
+ const defs = await (0, artifact_registry_1.listArtifactDefinitions)(cwd);
40
+ const loaded = [];
41
+ for (const def of defs) {
42
+ const type = def.name;
43
+ const active = await (0, output_index_1.getActiveArtifactPath)(cwd, type);
44
+ const fallback = async () => {
45
+ const files = await (0, utils_1.listFilesSortedByMtime)((0, paths_1.outputDirPath)(cwd, type, def.output_dir));
46
+ return files[0];
47
+ };
48
+ const latest = active ?? (await fallback());
49
+ if (!latest)
50
+ continue;
51
+ const parsed = await loadArtifactDoc(latest);
52
+ loaded.push({
53
+ type,
54
+ file: latest,
55
+ doc: parsed
56
+ });
57
+ }
58
+ return loaded;
59
+ }
60
+ async function listHtmlFiles(dir) {
61
+ if (!(await (0, utils_1.fileExists)(dir)))
62
+ return [];
63
+ const entries = await promises_1.default.readdir(dir, { withFileTypes: true });
64
+ return entries
65
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".html"))
66
+ .map((entry) => node_path_1.default.join(dir, entry.name));
67
+ }
68
+ function formatIssue(issue) {
69
+ const location = issue.file ? ` (${issue.file})` : "";
70
+ const field = issue.field ? ` [${issue.field}]` : "";
71
+ const check = issue.check ? ` [${issue.check}]` : "";
72
+ const fix = issue.suggestion ? `\n Suggestion: ${issue.suggestion}` : "";
73
+ return `- [${issue.level.toUpperCase()}] ${issue.code}${check}${field}: ${issue.message}${location}${fix}`;
74
+ }
75
+ async function writeReport(targetPath, issues) {
76
+ const status = issues.some((issue) => issue.level === "error") ? "FAIL" : "PASS";
77
+ const schemaIssues = issues.filter((issue) => issue.check === "schema");
78
+ const coverageIssues = issues.filter((issue) => issue.check === "tag_coverage");
79
+ const relevanceIssues = issues.filter((issue) => issue.check === "contract_relevance");
80
+ const semanticIssues = issues.filter((issue) => issue.check === "semantic_consistency");
81
+ const gate = (set) => (set.some((issue) => issue.level === "error") ? "FAIL" : "PASS");
82
+ const content = [
83
+ "# Prodo Validation Report",
84
+ "",
85
+ `Status: **${status}**`,
86
+ `Generated at: ${new Date().toISOString()}`,
87
+ "",
88
+ "## Gate Results",
89
+ `- Schema pass: ${gate(schemaIssues)}`,
90
+ `- Tag coverage pass: ${gate(coverageIssues)}`,
91
+ `- Contract relevance pass: ${gate(relevanceIssues)}`,
92
+ `- Semantic consistency pass: ${gate(semanticIssues)}`,
93
+ "",
94
+ "## Findings",
95
+ issues.length === 0 ? "- No issues found." : issues.map(formatIssue).join("\n"),
96
+ ""
97
+ ].join("\n");
98
+ await (0, utils_1.ensureDir)(node_path_1.default.dirname(targetPath));
99
+ await promises_1.default.writeFile(targetPath, content, "utf8");
100
+ }
101
+ async function runValidate(cwd, options) {
102
+ const normalizedPath = (0, paths_1.normalizedBriefPath)(cwd);
103
+ if (!(await (0, utils_1.fileExists)(normalizedPath))) {
104
+ throw new errors_1.UserError("Missing `.prodo/briefs/normalized-brief.json`. Run `prodo-init` and create it first.");
105
+ }
106
+ const normalizedBrief = await (0, utils_1.readJsonFile)(normalizedPath);
107
+ const loaded = await loadLatestArtifacts(cwd);
108
+ const issues = [];
109
+ for (const artifact of loaded) {
110
+ const template = await (0, template_resolver_1.resolveTemplate)({
111
+ cwd,
112
+ artifactType: artifact.type
113
+ });
114
+ const headings = template ? (0, template_resolver_1.extractRequiredHeadingsFromTemplate)(template.content) : [];
115
+ const schemaCheck = await (0, validator_1.validateSchema)(cwd, artifact.type, artifact.doc, headings);
116
+ issues.push(...schemaCheck.issues.map((issue) => ({ ...issue, file: artifact.file })));
117
+ if (artifact.type === "workflow") {
118
+ const ext = node_path_1.default.extname(artifact.file).toLowerCase();
119
+ if (ext !== ".md") {
120
+ issues.push({
121
+ level: "error",
122
+ code: "workflow_markdown_missing",
123
+ check: "schema",
124
+ artifactType: artifact.type,
125
+ file: artifact.file,
126
+ message: "Workflow explanation artifact must be Markdown (.md).",
127
+ suggestion: "Regenerate workflow so explanation is written to .md."
128
+ });
129
+ }
130
+ const mmdPath = node_path_1.default.join(node_path_1.default.dirname(artifact.file), `${node_path_1.default.parse(artifact.file).name}.mmd`);
131
+ if (!(await (0, utils_1.fileExists)(mmdPath))) {
132
+ issues.push({
133
+ level: "error",
134
+ code: "workflow_mermaid_missing",
135
+ check: "schema",
136
+ artifactType: artifact.type,
137
+ file: artifact.file,
138
+ message: "Workflow Mermaid companion file (.mmd) is missing.",
139
+ suggestion: "Regenerate workflow so markdown and .mmd are produced as a pair."
140
+ });
141
+ }
142
+ else {
143
+ const mmdRaw = await promises_1.default.readFile(mmdPath, "utf8");
144
+ const mermaidLike = /(^|\n)\s*flowchart\s+/i.test(mmdRaw) || /(^|\n)\s*graph\s+/i.test(mmdRaw);
145
+ if (!mermaidLike) {
146
+ issues.push({
147
+ level: "error",
148
+ code: "workflow_mermaid_invalid",
149
+ check: "schema",
150
+ artifactType: artifact.type,
151
+ file: mmdPath,
152
+ message: "Workflow Mermaid file is invalid or prose-only.",
153
+ suggestion: "Ensure .mmd file contains valid Mermaid diagram syntax."
154
+ });
155
+ }
156
+ }
157
+ }
158
+ if (artifact.type === "wireframe") {
159
+ const ext = node_path_1.default.extname(artifact.file).toLowerCase();
160
+ if (ext !== ".md") {
161
+ issues.push({
162
+ level: "error",
163
+ code: "wireframe_markdown_missing",
164
+ check: "schema",
165
+ artifactType: artifact.type,
166
+ file: artifact.file,
167
+ message: "Wireframe explanation artifact must be Markdown (.md).",
168
+ suggestion: "Regenerate wireframe so explanation is written to .md."
169
+ });
170
+ }
171
+ const htmlPath = node_path_1.default.join(node_path_1.default.dirname(artifact.file), `${node_path_1.default.parse(artifact.file).name}.html`);
172
+ if (!(await (0, utils_1.fileExists)(htmlPath))) {
173
+ issues.push({
174
+ level: "error",
175
+ code: "wireframe_html_missing",
176
+ check: "schema",
177
+ artifactType: artifact.type,
178
+ file: artifact.file,
179
+ message: "Wireframe HTML companion file is missing.",
180
+ suggestion: "Regenerate wireframe so markdown and .html are produced as a pair."
181
+ });
182
+ }
183
+ else {
184
+ const htmlRaw = await promises_1.default.readFile(htmlPath, "utf8");
185
+ const htmlLooksValid = /<!doctype html>/i.test(htmlRaw) || /<html[\s>]/i.test(htmlRaw);
186
+ if (!htmlLooksValid) {
187
+ issues.push({
188
+ level: "error",
189
+ code: "wireframe_html_invalid",
190
+ check: "schema",
191
+ artifactType: artifact.type,
192
+ file: htmlPath,
193
+ message: "Wireframe output is not valid HTML content.",
194
+ suggestion: "Ensure wireframe companion HTML contains a valid document structure."
195
+ });
196
+ }
197
+ }
198
+ const htmlFiles = await listHtmlFiles(node_path_1.default.dirname(artifact.file));
199
+ if (htmlFiles.length < 1) {
200
+ issues.push({
201
+ level: "error",
202
+ code: "wireframe_screens_missing",
203
+ check: "schema",
204
+ artifactType: artifact.type,
205
+ file: artifact.file,
206
+ message: "Wireframe must include at least one HTML screen artifact.",
207
+ suggestion: "Regenerate wireframe to create paired .md and .html screen files."
208
+ });
209
+ }
210
+ }
211
+ }
212
+ issues.push(...(await (0, consistency_1.checkConsistency)(cwd, loaded, normalizedBrief)));
213
+ if (options.strict) {
214
+ for (const issue of issues) {
215
+ if (issue.level === "warning")
216
+ issue.level = "error";
217
+ }
218
+ }
219
+ const finalReportPath = options.report ? node_path_1.default.resolve(cwd, options.report) : (0, paths_1.reportPath)(cwd);
220
+ if (!(0, utils_1.isPathInside)(node_path_1.default.join(cwd, "product-docs"), finalReportPath)) {
221
+ throw new errors_1.UserError("Validation report must be inside `product-docs/`.");
222
+ }
223
+ await writeReport(finalReportPath, issues);
224
+ const pass = !issues.some((issue) => issue.level === "error");
225
+ return { pass, reportPath: finalReportPath, issues };
226
+ }
@@ -0,0 +1,5 @@
1
+ import type { ArtifactDoc, ArtifactType, ValidationIssue } from "./types";
2
+ export declare function validateSchema(cwd: string, artifactType: ArtifactType, doc: ArtifactDoc, requiredHeadingsOverride?: string[]): Promise<{
3
+ issues: ValidationIssue[];
4
+ requiredHeadings: string[];
5
+ }>;
@@ -0,0 +1,76 @@
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.validateSchema = validateSchema;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const _2020_1 = __importDefault(require("ajv/dist/2020"));
9
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
10
+ const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ const markdown_1 = require("./markdown");
12
+ const paths_1 = require("./paths");
13
+ const ajv = new _2020_1.default({ allErrors: true, strict: false });
14
+ (0, ajv_formats_1.default)(ajv);
15
+ async function resolveSchema(cwd, artifactType) {
16
+ const raw = await promises_1.default.readFile((0, paths_1.schemaPath)(cwd, artifactType), "utf8");
17
+ return js_yaml_1.default.load(raw);
18
+ }
19
+ function requiredHeadingsFromSchema(schema) {
20
+ if (!Array.isArray(schema.x_required_headings))
21
+ return [];
22
+ return schema.x_required_headings.filter((item) => typeof item === "string");
23
+ }
24
+ async function validateSchema(cwd, artifactType, doc, requiredHeadingsOverride) {
25
+ const schema = await resolveSchema(cwd, artifactType);
26
+ const requiredHeadings = requiredHeadingsOverride && requiredHeadingsOverride.length > 0
27
+ ? requiredHeadingsOverride
28
+ : requiredHeadingsFromSchema(schema);
29
+ const workingSchema = { ...schema };
30
+ delete workingSchema.x_required_headings;
31
+ const validate = ajv.compile(workingSchema);
32
+ const valid = validate(doc);
33
+ const issues = [];
34
+ if (!valid && validate.errors) {
35
+ for (const err of validate.errors) {
36
+ issues.push({
37
+ level: "error",
38
+ code: "schema_validation_failed",
39
+ check: "schema",
40
+ artifactType,
41
+ field: err.instancePath || err.schemaPath,
42
+ message: `Schema validation error: ${err.message ?? "unknown error"}`,
43
+ suggestion: "Adjust the generated content to satisfy schema requirements."
44
+ });
45
+ }
46
+ }
47
+ const sections = (0, markdown_1.sectionTextMap)(doc.body);
48
+ for (const heading of requiredHeadings) {
49
+ if (!doc.body.includes(heading)) {
50
+ issues.push({
51
+ level: "error",
52
+ code: "missing_required_heading",
53
+ check: "schema",
54
+ artifactType,
55
+ field: heading,
56
+ message: `Required section missing: ${heading}`,
57
+ suggestion: "Regenerate or manually edit the artifact to include all required headings."
58
+ });
59
+ continue;
60
+ }
61
+ const content = sections.get(heading) ?? "";
62
+ const isPlaceholder = /(tbd|to be defined|i don't know|unknown|n\/a)/i.test(content);
63
+ if (content.trim().length < 20 || isPlaceholder) {
64
+ issues.push({
65
+ level: "error",
66
+ code: "weak_required_heading_content",
67
+ check: "schema",
68
+ artifactType,
69
+ field: heading,
70
+ message: `Section has weak or placeholder content: ${heading}`,
71
+ suggestion: "Replace placeholders with concrete, actionable details."
72
+ });
73
+ }
74
+ }
75
+ return { issues, requiredHeadings };
76
+ }