@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,303 +1,374 @@
1
- import path from "node:path";
2
- import { listArtifactTypes, getArtifactDefinition } from "./artifact-registry";
3
- import { contractIds, parseNormalizedBriefOrThrow } from "./normalized-brief";
4
- import { createProvider } from "./providers";
5
- import type { ArtifactDoc, ArtifactType, ContractCoverage, ValidationIssue } from "./types";
6
-
7
- type LoadedArtifact = {
8
- type: ArtifactType;
9
- file: string;
10
- doc: ArtifactDoc;
11
- };
12
-
13
- function asStringArray(value: unknown): string[] {
14
- if (!Array.isArray(value)) return [];
15
- return value.filter((item): item is string => typeof item === "string");
16
- }
17
-
18
- function readCoverage(frontmatter: Record<string, unknown>): ContractCoverage {
19
- const coverage = frontmatter.contract_coverage as Partial<ContractCoverage> | undefined;
20
- return {
21
- goals: asStringArray(coverage?.goals),
22
- core_features: asStringArray(coverage?.core_features),
23
- constraints: asStringArray(coverage?.constraints)
24
- };
25
- }
26
-
27
- async function checkMissingArtifacts(cwd: string, loadedArtifacts: LoadedArtifact[]): Promise<ValidationIssue[]> {
28
- const expectedTypes = await listArtifactTypes(cwd);
29
- const present = new Set(loadedArtifacts.map((item) => item.type));
30
- const missing = expectedTypes.filter((type) => !present.has(type));
31
- if (missing.length === 0) return [];
32
- return [
33
- {
34
- level: "warning",
35
- code: "missing_artifacts",
36
- check: "schema",
37
- message: `Some artifacts are missing from outputs: ${missing.join(", ")}`,
38
- suggestion: "Run the corresponding prodo-* commands before final validation."
39
- }
40
- ];
41
- }
42
-
43
- async function checkContractCoverage(
44
- cwd: string,
45
- loaded: LoadedArtifact[],
46
- normalizedBrief: Record<string, unknown>
47
- ): Promise<ValidationIssue[]> {
48
- const issues: ValidationIssue[] = [];
49
- const normalized = parseNormalizedBriefOrThrow(normalizedBrief);
50
- const expected = contractIds(normalized.contracts);
51
-
52
- for (const artifact of loaded) {
53
- const def = await getArtifactDefinition(cwd, artifact.type);
54
- const coverage = readCoverage(artifact.doc.frontmatter);
55
- for (const key of def.required_contracts) {
56
- const missing = expected[key].filter((id) => !coverage[key].includes(id));
57
- if (missing.length === 0) continue;
58
- issues.push({
59
- level: "error",
60
- code: "missing_contract_coverage",
61
- check: "tag_coverage",
62
- artifactType: artifact.type,
63
- file: artifact.file,
64
- field: `frontmatter.contract_coverage.${key}`,
65
- message: `Artifact is missing required contract IDs for ${key}: ${missing.join(", ")}`,
66
- suggestion: "Regenerate artifact and include explicit contract tags such as [G1], [F2], [C1]."
67
- });
68
- }
69
- }
70
-
71
- return issues;
72
- }
73
-
74
- function checkUpstreamReferences(loaded: LoadedArtifact[]): ValidationIssue[] {
75
- const issues: ValidationIssue[] = [];
76
- const filesByName = new Set(loaded.map((item) => path.normalize(item.file)));
77
-
78
- for (const artifact of loaded) {
79
- const upstream = artifact.doc.frontmatter.upstream_artifacts;
80
- if (!Array.isArray(upstream)) continue;
81
-
82
- for (const rawItem of upstream) {
83
- if (typeof rawItem !== "string") continue;
84
- const resolved = path.normalize(path.resolve(path.dirname(artifact.file), rawItem));
85
- if (!filesByName.has(resolved)) {
86
- issues.push({
87
- level: "error",
88
- code: "broken_upstream_reference",
89
- check: "schema",
90
- artifactType: artifact.type,
91
- file: artifact.file,
92
- field: "frontmatter.upstream_artifacts",
93
- message: `Referenced upstream artifact not found: ${rawItem}`,
94
- suggestion: "Regenerate this artifact or update upstream_artifacts paths to existing outputs."
95
- });
96
- }
97
- }
98
- }
99
-
100
- return issues;
101
- }
102
-
103
- function taggedLinesByContract(body: string): Array<{ contractId: string; line: string }> {
104
- const lines = body
105
- .split(/\r?\n/)
106
- .map((line) => line.trim())
107
- .filter((line) => line.length > 0);
108
- const tagged: Array<{ contractId: string; line: string }> = [];
109
- for (const line of lines) {
110
- const matches = line.match(/\[([GFC][0-9]+)\]/g) ?? [];
111
- for (const match of matches) {
112
- tagged.push({ contractId: match.slice(1, -1), line });
113
- }
114
- }
115
- return tagged;
116
- }
117
-
118
- function parseJsonObject<T>(raw: string, fallback: T): T {
119
- const trimmed = raw.trim();
120
- const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
121
- const candidate = fenced ? fenced[1] : trimmed;
122
- try {
123
- return JSON.parse(candidate) as T;
124
- } catch {
125
- return fallback;
126
- }
127
- }
128
-
129
- function hasEnglishLeak(body: string): boolean {
130
- const markers = [" the ", " and ", " with ", " user ", " should ", " must "];
131
- const normalized = ` ${body.toLowerCase().replace(/\s+/g, " ")} `;
132
- return markers.filter((item) => normalized.includes(item)).length >= 2;
133
- }
134
-
135
- function checkLanguageConsistency(loaded: LoadedArtifact[]): ValidationIssue[] {
136
- const issues: ValidationIssue[] = [];
137
- const languages = new Set<string>();
138
- for (const artifact of loaded) {
139
- const lang = String((artifact.doc.frontmatter.language ?? "")).toLowerCase();
140
- if (lang) languages.add(lang);
141
- if (lang.startsWith("tr") && hasEnglishLeak(artifact.doc.body)) {
142
- issues.push({
143
- level: "error",
144
- code: "language_mixed_content",
145
- check: "schema",
146
- artifactType: artifact.type,
147
- file: artifact.file,
148
- message: "Artifact contains mixed language content while target language is Turkish.",
149
- suggestion: "Regenerate artifact with strict Turkish output."
150
- });
151
- }
152
- }
153
- if (languages.size > 1) {
154
- issues.push({
155
- level: "error",
156
- code: "language_inconsistent_across_artifacts",
157
- check: "schema",
158
- message: "Artifacts have inconsistent language settings.",
159
- suggestion: "Regenerate artifacts so all frontmatter.language values match."
160
- });
161
- }
162
- return issues;
163
- }
164
-
165
- async function checkContractRelevance(
166
- loaded: LoadedArtifact[],
167
- normalizedBrief: Record<string, unknown>
168
- ): Promise<ValidationIssue[]> {
169
- const normalized = parseNormalizedBriefOrThrow(normalizedBrief);
170
- const contractMap = new Map<string, string>();
171
- for (const item of normalized.contracts.goals) contractMap.set(item.id, item.text);
172
- for (const item of normalized.contracts.core_features) contractMap.set(item.id, item.text);
173
- for (const item of normalized.contracts.constraints) contractMap.set(item.id, item.text);
174
-
175
- const provider = createProvider();
176
- const issues: ValidationIssue[] = [];
177
- for (const artifact of loaded) {
178
- const taggedLines = taggedLinesByContract(artifact.doc.body);
179
- for (const tagged of taggedLines) {
180
- const contractText = contractMap.get(tagged.contractId);
181
- if (!contractText) {
182
- issues.push({
183
- level: "error",
184
- code: "unknown_contract_tag",
185
- check: "contract_relevance",
186
- artifactType: artifact.type,
187
- file: artifact.file,
188
- field: tagged.contractId,
189
- message: `Unknown contract tag used: ${tagged.contractId}`,
190
- suggestion: "Use only contract IDs that exist in normalized brief contracts."
191
- });
192
- continue;
193
- }
194
-
195
- const response = await provider.generate(
196
- "Evaluate if tagged line semantically matches contract text.",
197
- {
198
- contract_id: tagged.contractId,
199
- contract_text: contractText,
200
- context_text: tagged.line
201
- },
202
- {
203
- artifactType: "contract_relevance",
204
- requiredHeadings: [],
205
- requiredContracts: []
206
- }
207
- );
208
-
209
- const verdict = parseJsonObject<{ relevant?: boolean; score?: number; reason?: string }>(response.body, {});
210
- const relevant = Boolean(verdict.relevant);
211
- if (!relevant) {
212
- issues.push({
213
- level: "error",
214
- code: "irrelevant_contract_tag_usage",
215
- check: "contract_relevance",
216
- artifactType: artifact.type,
217
- file: artifact.file,
218
- field: tagged.contractId,
219
- message: `Tag ${tagged.contractId} does not match nearby content semantically.`,
220
- suggestion: verdict.reason ?? "Rewrite the tagged sentence so it clearly addresses the referenced contract."
221
- });
222
- }
223
- }
224
- }
225
- return issues;
226
- }
227
-
228
- async function checkSemanticPairs(loaded: LoadedArtifact[]): Promise<ValidationIssue[]> {
229
- const byType = new Map<ArtifactType, LoadedArtifact>();
230
- for (const artifact of loaded) byType.set(artifact.type, artifact);
231
-
232
- const pairs: Array<[ArtifactType, ArtifactType]> = [
233
- ["prd", "stories"],
234
- ["workflow", "techspec"],
235
- ["workflow", "wireframe"]
236
- ];
237
- const provider = createProvider();
238
- const issues: ValidationIssue[] = [];
239
-
240
- for (const [leftType, rightType] of pairs) {
241
- const left = byType.get(leftType);
242
- const right = byType.get(rightType);
243
- if (!left || !right) continue;
244
-
245
- const result = await provider.generate(
246
- "Compare paired artifacts semantically and return contradictions.",
247
- {
248
- pair: {
249
- left_type: leftType,
250
- left_file: left.file,
251
- left_coverage: readCoverage(left.doc.frontmatter),
252
- left_body: left.doc.body,
253
- right_type: rightType,
254
- right_file: right.file,
255
- right_coverage: readCoverage(right.doc.frontmatter),
256
- right_body: right.doc.body
257
- }
258
- },
259
- {
260
- artifactType: "semantic_consistency",
261
- requiredHeadings: [],
262
- requiredContracts: []
263
- }
264
- );
265
-
266
- const parsed = parseJsonObject<{ issues?: Array<Record<string, unknown>> }>(result.body, { issues: [] });
267
- for (const item of parsed.issues ?? []) {
268
- issues.push({
269
- level: (item.level === "warning" ? "warning" : "error") as "error" | "warning",
270
- code: typeof item.code === "string" ? item.code : "semantic_inconsistency",
271
- check: "semantic_consistency",
272
- file: typeof item.file === "string" ? item.file : left.file,
273
- field: typeof item.contract_id === "string" ? item.contract_id : undefined,
274
- message:
275
- typeof item.message === "string"
276
- ? item.message
277
- : `Semantic mismatch between ${leftType} and ${rightType}.`,
278
- suggestion:
279
- typeof item.suggestion === "string"
280
- ? item.suggestion
281
- : `Align ${leftType} and ${rightType} decisions and regenerate.`
282
- });
283
- }
284
- }
285
-
286
- return issues;
287
- }
288
-
289
- export async function checkConsistency(
290
- cwd: string,
291
- loadedArtifacts: LoadedArtifact[],
292
- normalizedBrief: Record<string, unknown>
293
- ): Promise<ValidationIssue[]> {
294
- const baseIssues = [
295
- ...(await checkMissingArtifacts(cwd, loadedArtifacts)),
296
- ...(await checkContractCoverage(cwd, loadedArtifacts, normalizedBrief)),
297
- ...checkUpstreamReferences(loadedArtifacts),
298
- ...checkLanguageConsistency(loadedArtifacts)
299
- ];
300
- const relevanceIssues = await checkContractRelevance(loadedArtifacts, normalizedBrief);
301
- const semanticIssues = await checkSemanticPairs(loadedArtifacts);
302
- return [...baseIssues, ...relevanceIssues, ...semanticIssues];
303
- }
1
+ import path from "node:path";
2
+ import { listArtifactTypes, getArtifactDefinition } from "./artifact-registry";
3
+ import { parseMarkdownSections, taggedLinesByContract } from "./markdown";
4
+ import { contractIds, parseNormalizedBriefOrThrow, type NormalizedBrief } from "./normalized-brief";
5
+ import { createProvider } from "../providers";
6
+ import { buildTermMap, checkTermReconciliation } from "./terminology";
7
+ import { buildTraceMap, checkRequirementCompleteness } from "./tracing";
8
+ import type { ArtifactDoc, ArtifactType, ContractCoverage, ValidationIssue } from "./types";
9
+
10
+ type LoadedArtifact = {
11
+ type: ArtifactType;
12
+ file: string;
13
+ doc: ArtifactDoc;
14
+ };
15
+
16
+ function asStringArray(value: unknown): string[] {
17
+ if (!Array.isArray(value)) return [];
18
+ return value.filter((item): item is string => typeof item === "string");
19
+ }
20
+
21
+ function readCoverage(frontmatter: Record<string, unknown>): ContractCoverage {
22
+ const coverage = frontmatter.contract_coverage as Partial<ContractCoverage> | undefined;
23
+ return {
24
+ goals: asStringArray(coverage?.goals),
25
+ core_features: asStringArray(coverage?.core_features),
26
+ constraints: asStringArray(coverage?.constraints)
27
+ };
28
+ }
29
+
30
+ async function checkMissingArtifacts(cwd: string, loadedArtifacts: LoadedArtifact[]): Promise<ValidationIssue[]> {
31
+ const expectedTypes = await listArtifactTypes(cwd);
32
+ const present = new Set(loadedArtifacts.map((item) => item.type));
33
+ const missing = expectedTypes.filter((type) => !present.has(type));
34
+ if (missing.length === 0) return [];
35
+ return [
36
+ {
37
+ level: "warning",
38
+ code: "missing_artifacts",
39
+ check: "schema",
40
+ message: `Some artifacts are missing from outputs: ${missing.join(", ")}`,
41
+ suggestion: "Run the corresponding prodo-* commands before final validation."
42
+ }
43
+ ];
44
+ }
45
+
46
+ async function checkContractCoverage(
47
+ cwd: string,
48
+ loaded: LoadedArtifact[],
49
+ normalizedBrief: Record<string, unknown>
50
+ ): Promise<ValidationIssue[]> {
51
+ const issues: ValidationIssue[] = [];
52
+ const normalized = parseNormalizedBriefOrThrow(normalizedBrief);
53
+ const expected = contractIds(normalized.contracts);
54
+
55
+ for (const artifact of loaded) {
56
+ const def = await getArtifactDefinition(cwd, artifact.type);
57
+ const coverage = readCoverage(artifact.doc.frontmatter);
58
+ for (const key of def.required_contracts) {
59
+ const missing = expected[key].filter((id) => !coverage[key].includes(id));
60
+ if (missing.length === 0) continue;
61
+ issues.push({
62
+ level: "error",
63
+ code: "missing_contract_coverage",
64
+ check: "tag_coverage",
65
+ artifactType: artifact.type,
66
+ file: artifact.file,
67
+ field: `frontmatter.contract_coverage.${key}`,
68
+ message: `Artifact is missing required contract IDs for ${key}: ${missing.join(", ")}`,
69
+ suggestion: "Regenerate artifact and include explicit contract tags such as [G1], [F2], [C1]."
70
+ });
71
+ }
72
+ }
73
+
74
+ return issues;
75
+ }
76
+
77
+ function checkUpstreamReferences(loaded: LoadedArtifact[]): ValidationIssue[] {
78
+ const issues: ValidationIssue[] = [];
79
+ const filesByName = new Set(loaded.map((item) => path.normalize(item.file)));
80
+
81
+ for (const artifact of loaded) {
82
+ const upstream = artifact.doc.frontmatter.upstream_artifacts;
83
+ if (!Array.isArray(upstream)) continue;
84
+
85
+ for (const rawItem of upstream) {
86
+ if (typeof rawItem !== "string") continue;
87
+ const resolved = path.normalize(path.resolve(path.dirname(artifact.file), rawItem));
88
+ if (!filesByName.has(resolved)) {
89
+ issues.push({
90
+ level: "error",
91
+ code: "broken_upstream_reference",
92
+ check: "schema",
93
+ artifactType: artifact.type,
94
+ file: artifact.file,
95
+ field: "frontmatter.upstream_artifacts",
96
+ message: `Referenced upstream artifact not found: ${rawItem}`,
97
+ suggestion: "Regenerate this artifact or update upstream_artifacts paths to existing outputs."
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return issues;
104
+ }
105
+
106
+ // taggedLinesByContract is now imported from ./markdown
107
+
108
+ function parseJsonObject<T>(raw: string, fallback: T): T {
109
+ const trimmed = raw.trim();
110
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
111
+ const candidate = fenced ? fenced[1] : trimmed;
112
+ try {
113
+ return JSON.parse(candidate) as T;
114
+ } catch {
115
+ return fallback;
116
+ }
117
+ }
118
+
119
+ function hasEnglishLeak(body: string): boolean {
120
+ const markers = [" the ", " and ", " with ", " user ", " should ", " must "];
121
+ const normalized = ` ${body.toLowerCase().replace(/\s+/g, " ")} `;
122
+ return markers.filter((item) => normalized.includes(item)).length >= 2;
123
+ }
124
+
125
+ function checkLanguageConsistency(loaded: LoadedArtifact[]): ValidationIssue[] {
126
+ const issues: ValidationIssue[] = [];
127
+ const languages = new Set<string>();
128
+ for (const artifact of loaded) {
129
+ const lang = String((artifact.doc.frontmatter.language ?? "")).toLowerCase();
130
+ if (lang) languages.add(lang);
131
+ if (lang.startsWith("tr") && hasEnglishLeak(artifact.doc.body)) {
132
+ issues.push({
133
+ level: "error",
134
+ code: "language_mixed_content",
135
+ check: "schema",
136
+ artifactType: artifact.type,
137
+ file: artifact.file,
138
+ message: "Artifact contains mixed language content while target language is Turkish.",
139
+ suggestion: "Regenerate artifact with strict Turkish output."
140
+ });
141
+ }
142
+ }
143
+ if (languages.size > 1) {
144
+ issues.push({
145
+ level: "error",
146
+ code: "language_inconsistent_across_artifacts",
147
+ check: "schema",
148
+ message: "Artifacts have inconsistent language settings.",
149
+ suggestion: "Regenerate artifacts so all frontmatter.language values match."
150
+ });
151
+ }
152
+ return issues;
153
+ }
154
+
155
+ async function checkContractRelevance(
156
+ loaded: LoadedArtifact[],
157
+ normalizedBrief: Record<string, unknown>
158
+ ): Promise<ValidationIssue[]> {
159
+ const normalized = parseNormalizedBriefOrThrow(normalizedBrief);
160
+ const contractMap = new Map<string, string>();
161
+ for (const item of normalized.contracts.goals) contractMap.set(item.id, item.text);
162
+ for (const item of normalized.contracts.core_features) contractMap.set(item.id, item.text);
163
+ for (const item of normalized.contracts.constraints) contractMap.set(item.id, item.text);
164
+
165
+ const provider = createProvider();
166
+ const issues: ValidationIssue[] = [];
167
+ for (const artifact of loaded) {
168
+ const taggedLines = taggedLinesByContract(artifact.doc.body);
169
+ for (const tagged of taggedLines) {
170
+ const contractText = contractMap.get(tagged.contractId);
171
+ if (!contractText) {
172
+ issues.push({
173
+ level: "error",
174
+ code: "unknown_contract_tag",
175
+ check: "contract_relevance",
176
+ artifactType: artifact.type,
177
+ file: artifact.file,
178
+ field: tagged.contractId,
179
+ message: `Unknown contract tag used: ${tagged.contractId}`,
180
+ suggestion: "Use only contract IDs that exist in normalized brief contracts."
181
+ });
182
+ continue;
183
+ }
184
+
185
+ const response = await provider.generate(
186
+ "Evaluate if tagged line semantically matches contract text.",
187
+ {
188
+ contract_id: tagged.contractId,
189
+ contract_text: contractText,
190
+ context_text: tagged.line
191
+ },
192
+ {
193
+ artifactType: "contract_relevance",
194
+ requiredHeadings: [],
195
+ requiredContracts: []
196
+ }
197
+ );
198
+
199
+ const verdict = parseJsonObject<{ relevant?: boolean; score?: number; reason?: string }>(response.body, {});
200
+ const relevant = Boolean(verdict.relevant);
201
+ if (!relevant) {
202
+ issues.push({
203
+ level: "error",
204
+ code: "irrelevant_contract_tag_usage",
205
+ check: "contract_relevance",
206
+ artifactType: artifact.type,
207
+ file: artifact.file,
208
+ field: tagged.contractId,
209
+ message: `Tag ${tagged.contractId} does not match nearby content semantically.`,
210
+ suggestion: verdict.reason ?? "Rewrite the tagged sentence so it clearly addresses the referenced contract."
211
+ });
212
+ }
213
+ }
214
+ }
215
+ return issues;
216
+ }
217
+
218
+ async function checkSemanticPairs(loaded: LoadedArtifact[]): Promise<ValidationIssue[]> {
219
+ const byType = new Map<ArtifactType, LoadedArtifact>();
220
+ for (const artifact of loaded) byType.set(artifact.type, artifact);
221
+
222
+ const pairs: Array<[ArtifactType, ArtifactType]> = [
223
+ ["prd", "stories"],
224
+ ["workflow", "techspec"],
225
+ ["workflow", "wireframe"]
226
+ ];
227
+ const provider = createProvider();
228
+ const issues: ValidationIssue[] = [];
229
+
230
+ for (const [leftType, rightType] of pairs) {
231
+ const left = byType.get(leftType);
232
+ const right = byType.get(rightType);
233
+ if (!left || !right) continue;
234
+
235
+ const result = await provider.generate(
236
+ "Compare paired artifacts semantically and return contradictions.",
237
+ {
238
+ pair: {
239
+ left_type: leftType,
240
+ left_file: left.file,
241
+ left_coverage: readCoverage(left.doc.frontmatter),
242
+ left_body: left.doc.body,
243
+ right_type: rightType,
244
+ right_file: right.file,
245
+ right_coverage: readCoverage(right.doc.frontmatter),
246
+ right_body: right.doc.body
247
+ }
248
+ },
249
+ {
250
+ artifactType: "semantic_consistency",
251
+ requiredHeadings: [],
252
+ requiredContracts: []
253
+ }
254
+ );
255
+
256
+ const parsed = parseJsonObject<{ issues?: Array<Record<string, unknown>> }>(result.body, { issues: [] });
257
+ for (const item of parsed.issues ?? []) {
258
+ issues.push({
259
+ level: (item.level === "warning" ? "warning" : "error") as "error" | "warning",
260
+ code: typeof item.code === "string" ? item.code : "semantic_inconsistency",
261
+ check: "semantic_consistency",
262
+ file: typeof item.file === "string" ? item.file : left.file,
263
+ field: typeof item.contract_id === "string" ? item.contract_id : undefined,
264
+ message:
265
+ typeof item.message === "string"
266
+ ? item.message
267
+ : `Semantic mismatch between ${leftType} and ${rightType}.`,
268
+ suggestion:
269
+ typeof item.suggestion === "string"
270
+ ? item.suggestion
271
+ : `Align ${leftType} and ${rightType} decisions and regenerate.`
272
+ });
273
+ }
274
+ }
275
+
276
+ return issues;
277
+ }
278
+
279
+ function checkCrossReferences(loadedArtifacts: LoadedArtifact[]): ValidationIssue[] {
280
+ const issues: ValidationIssue[] = [];
281
+ const artifactTypeNames = new Set(loadedArtifacts.map((a) => a.type));
282
+
283
+ const sectionsByType = new Map<ArtifactType, Set<string>>();
284
+ for (const artifact of loadedArtifacts) {
285
+ const sections = parseMarkdownSections(artifact.doc.body);
286
+ const headingSet = new Set(sections.map((s) => s.headingKey));
287
+ sectionsByType.set(artifact.type, headingSet);
288
+ }
289
+
290
+ const crossRefPattern = /(?:see|refer to|as (?:defined|described|specified) in)\s+(prd|workflow|wireframe|stories|techspec)(?:\s+(?:section\s+)?[""]?([^"".,)\n]+)[""]?)?/gi;
291
+
292
+ for (const artifact of loadedArtifacts) {
293
+ const matches = artifact.doc.body.matchAll(crossRefPattern);
294
+ for (const match of matches) {
295
+ const refType = match[1].toLowerCase();
296
+ const refSection = match[2]?.trim();
297
+
298
+ if (!artifactTypeNames.has(refType)) {
299
+ issues.push({
300
+ level: "warning",
301
+ code: "broken_cross_reference",
302
+ check: "cross_reference",
303
+ artifactType: artifact.type,
304
+ file: artifact.file,
305
+ message: `Cross-reference to "${refType}" but that artifact does not exist.`,
306
+ suggestion: `Generate the ${refType} artifact or remove the cross-reference.`
307
+ });
308
+ continue;
309
+ }
310
+
311
+ if (refSection) {
312
+ const targetSections = sectionsByType.get(refType);
313
+ if (targetSections) {
314
+ const normalizedRef = refSection.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
315
+ const found = [...targetSections].some((s) => s.includes(normalizedRef) || normalizedRef.includes(s));
316
+ if (!found) {
317
+ issues.push({
318
+ level: "warning",
319
+ code: "broken_cross_reference",
320
+ check: "cross_reference",
321
+ artifactType: artifact.type,
322
+ file: artifact.file,
323
+ message: `Cross-reference to "${refType} section ${refSection}" but that section was not found.`,
324
+ suggestion: `Verify the section name or update the cross-reference.`
325
+ });
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+
332
+ return issues;
333
+ }
334
+
335
+ export async function checkConsistency(
336
+ cwd: string,
337
+ loadedArtifacts: LoadedArtifact[],
338
+ normalizedBrief: Record<string, unknown>
339
+ ): Promise<ValidationIssue[]> {
340
+ const baseIssues = [
341
+ ...(await checkMissingArtifacts(cwd, loadedArtifacts)),
342
+ ...(await checkContractCoverage(cwd, loadedArtifacts, normalizedBrief)),
343
+ ...checkUpstreamReferences(loadedArtifacts),
344
+ ...checkLanguageConsistency(loadedArtifacts)
345
+ ];
346
+
347
+ const relevanceIssues = await checkContractRelevance(loadedArtifacts, normalizedBrief);
348
+ const semanticIssues = await checkSemanticPairs(loadedArtifacts);
349
+
350
+ const crossRefIssues = checkCrossReferences(loadedArtifacts);
351
+
352
+ let terminologyIssues: ValidationIssue[] = [];
353
+ let tracingIssues: ValidationIssue[] = [];
354
+ try {
355
+ const parsed = parseNormalizedBriefOrThrow(normalizedBrief);
356
+ const termMap = buildTermMap(parsed, loadedArtifacts);
357
+ terminologyIssues = checkTermReconciliation(termMap);
358
+
359
+ const traceMap = buildTraceMap(parsed, loadedArtifacts);
360
+ const presentTypes = loadedArtifacts.map((a) => a.type);
361
+ tracingIssues = checkRequirementCompleteness(traceMap, parsed, presentTypes);
362
+ } catch {
363
+ // normalized brief parse failed — skip terminology/tracing (other checks will catch it)
364
+ }
365
+
366
+ return [
367
+ ...baseIssues,
368
+ ...relevanceIssues,
369
+ ...semanticIssues,
370
+ ...crossRefIssues,
371
+ ...terminologyIssues,
372
+ ...tracingIssues
373
+ ];
374
+ }