@shahmarasy/prodo 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -97
- package/bin/prodo.cjs +6 -6
- package/dist/agents/agent-registry.d.ts +13 -0
- package/dist/agents/agent-registry.js +79 -0
- package/dist/agents/anthropic/index.d.ts +9 -0
- package/dist/agents/anthropic/index.js +55 -0
- package/dist/agents/base.d.ts +25 -0
- package/dist/agents/base.js +71 -0
- package/dist/agents/google/index.d.ts +9 -0
- package/dist/agents/google/index.js +53 -0
- package/dist/agents/mock/index.d.ts +11 -0
- package/dist/agents/mock/index.js +26 -0
- package/dist/agents/openai/index.d.ts +9 -0
- package/dist/agents/openai/index.js +57 -0
- package/dist/agents/system-prompts.d.ts +3 -0
- package/dist/agents/system-prompts.js +32 -0
- package/dist/agents.js +4 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +265 -31
- package/dist/cli/agent-command-installer.d.ts +4 -0
- package/dist/cli/agent-command-installer.js +148 -0
- package/dist/cli/agent-ids.d.ts +15 -0
- package/dist/cli/agent-ids.js +49 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +144 -0
- package/dist/cli/fix-tui.d.ts +4 -0
- package/dist/cli/fix-tui.js +79 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +465 -0
- package/dist/cli/init-tui.d.ts +23 -0
- package/dist/cli/init-tui.js +176 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.js +334 -0
- package/dist/cli/normalize-interactive.d.ts +8 -0
- package/dist/cli/normalize-interactive.js +167 -0
- package/dist/cli/preset-loader.d.ts +4 -0
- package/dist/cli/preset-loader.js +210 -0
- package/dist/cli.js +80 -3
- package/dist/core/artifact-registry.d.ts +11 -0
- package/dist/core/artifact-registry.js +49 -0
- package/dist/core/artifacts.d.ts +10 -0
- package/dist/core/artifacts.js +892 -0
- package/dist/core/clean.d.ts +10 -0
- package/dist/core/clean.js +74 -0
- package/dist/core/consistency.d.ts +8 -0
- package/dist/core/consistency.js +328 -0
- package/dist/core/constants.d.ts +7 -0
- package/dist/core/constants.js +64 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/fix.d.ts +31 -0
- package/dist/core/fix.js +188 -0
- package/dist/core/hook-executor.d.ts +1 -0
- package/dist/core/hook-executor.js +175 -0
- package/dist/core/markdown.d.ts +16 -0
- package/dist/core/markdown.js +81 -0
- package/dist/core/normalize.d.ts +8 -0
- package/dist/core/normalize.js +125 -0
- package/dist/core/normalized-brief.d.ts +48 -0
- package/dist/core/normalized-brief.js +182 -0
- package/dist/core/output-index.d.ts +13 -0
- package/dist/core/output-index.js +55 -0
- package/dist/core/paths.d.ts +17 -0
- package/dist/core/paths.js +80 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +69 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/registry.js +115 -0
- package/dist/core/settings.d.ts +7 -0
- package/dist/core/settings.js +35 -0
- package/dist/core/template-engine.d.ts +3 -0
- package/dist/core/template-engine.js +43 -0
- package/dist/core/template-resolver.d.ts +15 -0
- package/dist/core/template-resolver.js +46 -0
- package/dist/core/templates.d.ts +33 -0
- package/dist/core/templates.js +440 -0
- package/dist/core/terminology.d.ts +21 -0
- package/dist/core/terminology.js +143 -0
- package/dist/core/tracing.d.ts +21 -0
- package/dist/core/tracing.js +74 -0
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.js +5 -0
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +66 -0
- package/dist/core/validate.d.ts +10 -0
- package/dist/core/validate.js +226 -0
- package/dist/core/validator.d.ts +5 -0
- package/dist/core/validator.js +76 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +30 -0
- package/dist/core/workflow-commands.d.ts +7 -0
- package/dist/core/workflow-commands.js +29 -0
- package/dist/i18n/en.json +45 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +63 -0
- package/dist/i18n/tr.json +45 -0
- package/dist/init-tui.d.ts +3 -0
- package/dist/init-tui.js +28 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +9 -3
- package/dist/normalize.js +55 -7
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.js +20 -6
- package/dist/providers/mock-provider.d.ts +1 -1
- package/dist/providers/mock-provider.js +7 -6
- package/dist/providers/openai-provider.d.ts +1 -1
- package/dist/providers/openai-provider.js +3 -2
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -1
- package/dist/skills/engine.d.ts +10 -0
- package/dist/skills/engine.js +75 -0
- package/dist/skills/fix-skill.d.ts +2 -0
- package/dist/skills/fix-skill.js +38 -0
- package/dist/skills/generate-artifact-skill.d.ts +2 -0
- package/dist/skills/generate-artifact-skill.js +32 -0
- package/dist/skills/generate-pipeline-skill.d.ts +2 -0
- package/dist/skills/generate-pipeline-skill.js +45 -0
- package/dist/skills/normalize-skill.d.ts +2 -0
- package/dist/skills/normalize-skill.js +29 -0
- package/dist/skills/types.d.ts +28 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/validate-skill.d.ts +2 -0
- package/dist/skills/validate-skill.js +29 -0
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/validator.js +0 -4
- package/dist/workflow-commands.js +2 -1
- package/package.json +74 -45
- package/presets/fintech/preset.json +48 -1
- package/presets/fintech/prompts/prd.md +99 -2
- package/presets/marketplace/preset.json +51 -1
- package/presets/marketplace/prompts/prd.md +140 -2
- package/presets/saas/preset.json +53 -1
- package/presets/saas/prompts/prd.md +150 -2
- package/src/agents/agent-registry.ts +93 -0
- package/src/agents/anthropic/index.ts +86 -0
- package/src/agents/anthropic/manifest.json +7 -0
- package/src/agents/base.ts +77 -0
- package/src/agents/google/index.ts +79 -0
- package/src/agents/google/manifest.json +7 -0
- package/src/agents/mock/index.ts +32 -0
- package/src/agents/mock/manifest.json +7 -0
- package/src/agents/openai/index.ts +83 -0
- package/src/agents/openai/manifest.json +7 -0
- package/src/agents/system-prompts.ts +35 -0
- package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
- package/src/{agents.ts → cli/agent-ids.ts} +58 -56
- package/src/{doctor.ts → cli/doctor.ts} +157 -137
- package/src/cli/fix-tui.ts +111 -0
- package/src/{cli.ts → cli/index.ts} +459 -319
- package/src/{init-tui.ts → cli/init-tui.ts} +208 -179
- package/src/{init.ts → cli/init.ts} +398 -391
- package/src/cli/normalize-interactive.ts +241 -0
- package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
- package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
- package/src/{artifacts.ts → core/artifacts.ts} +1081 -777
- package/src/core/clean.ts +88 -0
- package/src/{consistency.ts → core/consistency.ts} +374 -303
- package/src/{constants.ts → core/constants.ts} +72 -72
- package/src/{errors.ts → core/errors.ts} +7 -7
- package/src/core/fix.ts +253 -0
- package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
- package/src/{markdown.ts → core/markdown.ts} +93 -73
- package/src/core/normalize.ts +145 -0
- package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
- package/src/{output-index.ts → core/output-index.ts} +59 -59
- package/src/{paths.ts → core/paths.ts} +75 -71
- package/src/{project-config.ts → core/project-config.ts} +78 -78
- package/src/{registry.ts → core/registry.ts} +119 -119
- package/src/{settings.ts → core/settings.ts} +35 -34
- package/src/core/template-engine.ts +45 -0
- package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
- package/src/{templates.ts → core/templates.ts} +452 -450
- package/src/core/terminology.ts +177 -0
- package/src/core/tracing.ts +110 -0
- package/src/{types.ts → core/types.ts} +46 -46
- package/src/{utils.ts → core/utils.ts} +64 -50
- package/src/{validate.ts → core/validate.ts} +252 -246
- package/src/{validator.ts → core/validator.ts} +92 -96
- package/src/{version.ts → core/version.ts} +24 -24
- package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -31
- package/src/i18n/en.json +45 -0
- package/src/i18n/index.ts +58 -0
- package/src/i18n/tr.json +45 -0
- package/src/providers/index.ts +29 -12
- package/src/providers/mock-provider.ts +200 -199
- package/src/providers/openai-provider.ts +88 -87
- package/src/skills/engine.ts +94 -0
- package/src/skills/fix-skill.ts +38 -0
- package/src/skills/generate-artifact-skill.ts +32 -0
- package/src/skills/generate-pipeline-skill.ts +49 -0
- package/src/skills/normalize-skill.ts +29 -0
- package/src/skills/types.ts +36 -0
- package/src/skills/validate-skill.ts +29 -0
- package/templates/commands/prodo-fix.md +46 -0
- package/templates/commands/prodo-normalize.md +118 -23
- package/templates/commands/prodo-prd.md +138 -17
- package/templates/commands/prodo-stories.md +153 -17
- package/templates/commands/prodo-techspec.md +167 -17
- package/templates/commands/prodo-validate.md +184 -26
- package/templates/commands/prodo-wireframe.md +188 -17
- package/templates/commands/prodo-workflow.md +200 -17
- package/src/normalize.ts +0 -89
|
@@ -1,303 +1,374 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { listArtifactTypes, getArtifactDefinition } from "./artifact-registry";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
{
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
for (const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
+
}
|