@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.
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/agents.js +4 -2
  18. package/dist/artifacts.d.ts +1 -0
  19. package/dist/artifacts.js +265 -31
  20. package/dist/cli/agent-command-installer.d.ts +4 -0
  21. package/dist/cli/agent-command-installer.js +148 -0
  22. package/dist/cli/agent-ids.d.ts +15 -0
  23. package/dist/cli/agent-ids.js +49 -0
  24. package/dist/cli/doctor.d.ts +1 -0
  25. package/dist/cli/doctor.js +144 -0
  26. package/dist/cli/fix-tui.d.ts +4 -0
  27. package/dist/cli/fix-tui.js +79 -0
  28. package/dist/cli/index.d.ts +9 -0
  29. package/dist/cli/index.js +465 -0
  30. package/dist/cli/init-tui.d.ts +23 -0
  31. package/dist/cli/init-tui.js +176 -0
  32. package/dist/cli/init.d.ts +11 -0
  33. package/dist/cli/init.js +334 -0
  34. package/dist/cli/normalize-interactive.d.ts +8 -0
  35. package/dist/cli/normalize-interactive.js +167 -0
  36. package/dist/cli/preset-loader.d.ts +4 -0
  37. package/dist/cli/preset-loader.js +210 -0
  38. package/dist/cli.js +80 -3
  39. package/dist/core/artifact-registry.d.ts +11 -0
  40. package/dist/core/artifact-registry.js +49 -0
  41. package/dist/core/artifacts.d.ts +10 -0
  42. package/dist/core/artifacts.js +892 -0
  43. package/dist/core/clean.d.ts +10 -0
  44. package/dist/core/clean.js +74 -0
  45. package/dist/core/consistency.d.ts +8 -0
  46. package/dist/core/consistency.js +328 -0
  47. package/dist/core/constants.d.ts +7 -0
  48. package/dist/core/constants.js +64 -0
  49. package/dist/core/errors.d.ts +3 -0
  50. package/dist/core/errors.js +10 -0
  51. package/dist/core/fix.d.ts +31 -0
  52. package/dist/core/fix.js +188 -0
  53. package/dist/core/hook-executor.d.ts +1 -0
  54. package/dist/core/hook-executor.js +175 -0
  55. package/dist/core/markdown.d.ts +16 -0
  56. package/dist/core/markdown.js +81 -0
  57. package/dist/core/normalize.d.ts +8 -0
  58. package/dist/core/normalize.js +125 -0
  59. package/dist/core/normalized-brief.d.ts +48 -0
  60. package/dist/core/normalized-brief.js +182 -0
  61. package/dist/core/output-index.d.ts +13 -0
  62. package/dist/core/output-index.js +55 -0
  63. package/dist/core/paths.d.ts +17 -0
  64. package/dist/core/paths.js +80 -0
  65. package/dist/core/project-config.d.ts +14 -0
  66. package/dist/core/project-config.js +69 -0
  67. package/dist/core/registry.d.ts +13 -0
  68. package/dist/core/registry.js +115 -0
  69. package/dist/core/settings.d.ts +7 -0
  70. package/dist/core/settings.js +35 -0
  71. package/dist/core/template-engine.d.ts +3 -0
  72. package/dist/core/template-engine.js +43 -0
  73. package/dist/core/template-resolver.d.ts +15 -0
  74. package/dist/core/template-resolver.js +46 -0
  75. package/dist/core/templates.d.ts +33 -0
  76. package/dist/core/templates.js +440 -0
  77. package/dist/core/terminology.d.ts +21 -0
  78. package/dist/core/terminology.js +143 -0
  79. package/dist/core/tracing.d.ts +21 -0
  80. package/dist/core/tracing.js +74 -0
  81. package/dist/core/types.d.ts +35 -0
  82. package/dist/core/types.js +5 -0
  83. package/dist/core/utils.d.ts +7 -0
  84. package/dist/core/utils.js +66 -0
  85. package/dist/core/validate.d.ts +10 -0
  86. package/dist/core/validate.js +226 -0
  87. package/dist/core/validator.d.ts +5 -0
  88. package/dist/core/validator.js +76 -0
  89. package/dist/core/version.d.ts +1 -0
  90. package/dist/core/version.js +30 -0
  91. package/dist/core/workflow-commands.d.ts +7 -0
  92. package/dist/core/workflow-commands.js +29 -0
  93. package/dist/i18n/en.json +45 -0
  94. package/dist/i18n/index.d.ts +5 -0
  95. package/dist/i18n/index.js +63 -0
  96. package/dist/i18n/tr.json +45 -0
  97. package/dist/init-tui.d.ts +3 -0
  98. package/dist/init-tui.js +28 -1
  99. package/dist/init.d.ts +1 -0
  100. package/dist/init.js +9 -3
  101. package/dist/normalize.js +55 -7
  102. package/dist/providers/index.d.ts +2 -1
  103. package/dist/providers/index.js +20 -6
  104. package/dist/providers/mock-provider.d.ts +1 -1
  105. package/dist/providers/mock-provider.js +7 -6
  106. package/dist/providers/openai-provider.d.ts +1 -1
  107. package/dist/providers/openai-provider.js +3 -2
  108. package/dist/settings.d.ts +1 -0
  109. package/dist/settings.js +2 -1
  110. package/dist/skills/engine.d.ts +10 -0
  111. package/dist/skills/engine.js +75 -0
  112. package/dist/skills/fix-skill.d.ts +2 -0
  113. package/dist/skills/fix-skill.js +38 -0
  114. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  115. package/dist/skills/generate-artifact-skill.js +32 -0
  116. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  117. package/dist/skills/generate-pipeline-skill.js +45 -0
  118. package/dist/skills/normalize-skill.d.ts +2 -0
  119. package/dist/skills/normalize-skill.js +29 -0
  120. package/dist/skills/types.d.ts +28 -0
  121. package/dist/skills/types.js +2 -0
  122. package/dist/skills/validate-skill.d.ts +2 -0
  123. package/dist/skills/validate-skill.js +29 -0
  124. package/dist/templates.d.ts +1 -1
  125. package/dist/templates.js +2 -0
  126. package/dist/utils.d.ts +1 -0
  127. package/dist/utils.js +13 -0
  128. package/dist/validator.js +0 -4
  129. package/dist/workflow-commands.js +2 -1
  130. package/package.json +74 -45
  131. package/presets/fintech/preset.json +48 -1
  132. package/presets/fintech/prompts/prd.md +99 -2
  133. package/presets/marketplace/preset.json +51 -1
  134. package/presets/marketplace/prompts/prd.md +140 -2
  135. package/presets/saas/preset.json +53 -1
  136. package/presets/saas/prompts/prd.md +150 -2
  137. package/src/agents/agent-registry.ts +93 -0
  138. package/src/agents/anthropic/index.ts +86 -0
  139. package/src/agents/anthropic/manifest.json +7 -0
  140. package/src/agents/base.ts +77 -0
  141. package/src/agents/google/index.ts +79 -0
  142. package/src/agents/google/manifest.json +7 -0
  143. package/src/agents/mock/index.ts +32 -0
  144. package/src/agents/mock/manifest.json +7 -0
  145. package/src/agents/openai/index.ts +83 -0
  146. package/src/agents/openai/manifest.json +7 -0
  147. package/src/agents/system-prompts.ts +35 -0
  148. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  149. package/src/{agents.ts → cli/agent-ids.ts} +58 -56
  150. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  151. package/src/cli/fix-tui.ts +111 -0
  152. package/src/{cli.ts → cli/index.ts} +459 -319
  153. package/src/{init-tui.ts → cli/init-tui.ts} +208 -179
  154. package/src/{init.ts → cli/init.ts} +398 -391
  155. package/src/cli/normalize-interactive.ts +241 -0
  156. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  157. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  158. package/src/{artifacts.ts → core/artifacts.ts} +1081 -777
  159. package/src/core/clean.ts +88 -0
  160. package/src/{consistency.ts → core/consistency.ts} +374 -303
  161. package/src/{constants.ts → core/constants.ts} +72 -72
  162. package/src/{errors.ts → core/errors.ts} +7 -7
  163. package/src/core/fix.ts +253 -0
  164. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  165. package/src/{markdown.ts → core/markdown.ts} +93 -73
  166. package/src/core/normalize.ts +145 -0
  167. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  168. package/src/{output-index.ts → core/output-index.ts} +59 -59
  169. package/src/{paths.ts → core/paths.ts} +75 -71
  170. package/src/{project-config.ts → core/project-config.ts} +78 -78
  171. package/src/{registry.ts → core/registry.ts} +119 -119
  172. package/src/{settings.ts → core/settings.ts} +35 -34
  173. package/src/core/template-engine.ts +45 -0
  174. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  175. package/src/{templates.ts → core/templates.ts} +452 -450
  176. package/src/core/terminology.ts +177 -0
  177. package/src/core/tracing.ts +110 -0
  178. package/src/{types.ts → core/types.ts} +46 -46
  179. package/src/{utils.ts → core/utils.ts} +64 -50
  180. package/src/{validate.ts → core/validate.ts} +252 -246
  181. package/src/{validator.ts → core/validator.ts} +92 -96
  182. package/src/{version.ts → core/version.ts} +24 -24
  183. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -31
  184. package/src/i18n/en.json +45 -0
  185. package/src/i18n/index.ts +58 -0
  186. package/src/i18n/tr.json +45 -0
  187. package/src/providers/index.ts +29 -12
  188. package/src/providers/mock-provider.ts +200 -199
  189. package/src/providers/openai-provider.ts +88 -87
  190. package/src/skills/engine.ts +94 -0
  191. package/src/skills/fix-skill.ts +38 -0
  192. package/src/skills/generate-artifact-skill.ts +32 -0
  193. package/src/skills/generate-pipeline-skill.ts +49 -0
  194. package/src/skills/normalize-skill.ts +29 -0
  195. package/src/skills/types.ts +36 -0
  196. package/src/skills/validate-skill.ts +29 -0
  197. package/templates/commands/prodo-fix.md +46 -0
  198. package/templates/commands/prodo-normalize.md +118 -23
  199. package/templates/commands/prodo-prd.md +138 -17
  200. package/templates/commands/prodo-stories.md +153 -17
  201. package/templates/commands/prodo-techspec.md +167 -17
  202. package/templates/commands/prodo-validate.md +184 -26
  203. package/templates/commands/prodo-wireframe.md +188 -17
  204. package/templates/commands/prodo-workflow.md +200 -17
  205. package/src/normalize.ts +0 -89
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { UserError } from "./errors";
4
+ import {
5
+ buildContractsFromArrays,
6
+ parseNormalizedBriefOrThrow,
7
+ requireConfidenceOrThrow
8
+ } from "./normalized-brief";
9
+ import { briefPath, normalizedBriefPath, prodoPath } from "./paths";
10
+ import { createProvider } from "../providers";
11
+ import { readSettings } from "./settings";
12
+ import { fileExists, isPathInside } from "./utils";
13
+
14
+ type NormalizeOptions = {
15
+ cwd: string;
16
+ brief?: string;
17
+ out?: string;
18
+ additionalContext?: Record<string, string>;
19
+ };
20
+
21
+ function normalizedKey(value: string): string {
22
+ return value
23
+ .normalize("NFD")
24
+ .replace(/[\u0300-\u036f]/g, "")
25
+ .replace(/ı/g, "i")
26
+ .replace(/İ/g, "I")
27
+ .toLowerCase()
28
+ .replace(/[^a-z0-9]+/g, " ")
29
+ .trim();
30
+ }
31
+
32
+ function extractBriefProductName(rawBrief: string): string | undefined {
33
+ const lines = rawBrief.split(/\r?\n/);
34
+ for (let index = 0; index < lines.length; index += 1) {
35
+ const headingMatch = lines[index].match(/^\s*#{1,6}\s+(.+?)\s*$/);
36
+ if (!headingMatch) continue;
37
+ const headingKey = normalizedKey(headingMatch[1]);
38
+ const isProductHeading =
39
+ headingKey === "product name" ||
40
+ headingKey === "project name" ||
41
+ headingKey === "urun adi" ||
42
+ headingKey === "urun ismi";
43
+ if (!isProductHeading) continue;
44
+
45
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
46
+ const rawLine = lines[cursor].trim();
47
+ if (!rawLine) continue;
48
+ if (/^\s*#{1,6}\s+/.test(rawLine)) break;
49
+ const cleaned = rawLine.replace(/^\s*[-*]\s*/, "").trim();
50
+ if (cleaned.length > 0) return cleaned;
51
+ }
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function preserveOriginalProductName(
57
+ parsed: Record<string, unknown>,
58
+ rawBrief: string
59
+ ): Record<string, unknown> {
60
+ const briefProductName = extractBriefProductName(rawBrief);
61
+ if (!briefProductName) return parsed;
62
+ const generated = typeof parsed.product_name === "string" ? parsed.product_name : "";
63
+ if (!generated.trim()) return { ...parsed, product_name: briefProductName };
64
+ if (normalizedKey(generated) !== normalizedKey(briefProductName)) return parsed;
65
+ return { ...parsed, product_name: briefProductName };
66
+ }
67
+
68
+ function extractJsonObject(raw: string): Record<string, unknown> {
69
+ const trimmed = raw.trim();
70
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
71
+ const candidate = fenced ? fenced[1] : trimmed;
72
+ try {
73
+ return JSON.parse(candidate) as Record<string, unknown>;
74
+ } catch {
75
+ throw new UserError("Normalizer provider did not return valid JSON.");
76
+ }
77
+ }
78
+
79
+ export async function runNormalize(options: NormalizeOptions): Promise<string> {
80
+ const { cwd } = options;
81
+ const root = prodoPath(cwd);
82
+ if (!(await fileExists(root))) {
83
+ throw new UserError("Missing .prodo directory. Run `prodo init .` first.");
84
+ }
85
+
86
+ const inPath = options.brief ? path.resolve(cwd, options.brief) : briefPath(cwd);
87
+ if (!(await fileExists(inPath))) {
88
+ throw new UserError(`Brief file not found: ${inPath}`);
89
+ }
90
+
91
+ const rawBrief = await fs.readFile(inPath, "utf8");
92
+ const normalizePromptPath = path.join(root, "prompts", "normalize.md");
93
+ const normalizePrompt = await fs.readFile(normalizePromptPath, "utf8");
94
+ const settings = await readSettings(cwd);
95
+ const provider = createProvider();
96
+
97
+ const inputContext: Record<string, unknown> = {
98
+ briefMarkdown: rawBrief,
99
+ sourceBriefPath: inPath,
100
+ outputLanguage: settings.lang
101
+ };
102
+ if (options.additionalContext && Object.keys(options.additionalContext).length > 0) {
103
+ inputContext.userClarifications = options.additionalContext;
104
+ }
105
+
106
+ const generated = await provider.generate(
107
+ normalizePrompt,
108
+ inputContext,
109
+ {
110
+ artifactType: "normalize",
111
+ requiredHeadings: [],
112
+ requiredContracts: []
113
+ }
114
+ );
115
+
116
+ const parsed = extractJsonObject(generated.body);
117
+ const preserved = preserveOriginalProductName(parsed, rawBrief);
118
+ const withContracts = {
119
+ ...preserved,
120
+ contracts:
121
+ preserved.contracts ??
122
+ buildContractsFromArrays({
123
+ goals: Array.isArray(preserved.goals) ? preserved.goals.filter((x): x is string => typeof x === "string") : [],
124
+ core_features: Array.isArray(preserved.core_features)
125
+ ? preserved.core_features.filter((x): x is string => typeof x === "string")
126
+ : [],
127
+ constraints: Array.isArray(preserved.constraints)
128
+ ? preserved.constraints.filter((x): x is string => typeof x === "string")
129
+ : []
130
+ })
131
+ };
132
+
133
+ const normalized = parseNormalizedBriefOrThrow(withContracts);
134
+
135
+ const outPath = options.out ? path.resolve(cwd, options.out) : normalizedBriefPath(cwd);
136
+ if (!isPathInside(prodoPath(cwd), outPath)) {
137
+ throw new UserError("Normalize output must be inside `.prodo/`.");
138
+ }
139
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
140
+ await fs.writeFile(outPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
141
+
142
+ requireConfidenceOrThrow(normalized, ["product_name", "problem", "audience", "goals", "core_features"], 0.7);
143
+
144
+ return outPath;
145
+ }
@@ -1,206 +1,227 @@
1
- import Ajv2020 from "ajv/dist/2020";
2
- import { UserError } from "./errors";
3
- import type { ValidationIssue } from "./types";
4
-
5
- export type BriefContractItem = {
6
- id: string;
7
- text: string;
8
- };
9
-
10
- export type NormalizedBrief = {
11
- schema_version: string;
12
- product_name: string;
13
- problem: string;
14
- audience: string[];
15
- goals: string[];
16
- core_features: string[];
17
- constraints: string[];
18
- assumptions: string[];
19
- contracts: {
20
- goals: BriefContractItem[];
21
- core_features: BriefContractItem[];
22
- constraints: BriefContractItem[];
23
- };
24
- confidence?: Record<string, number>;
25
- };
26
-
27
- type BriefContracts = NormalizedBrief["contracts"];
28
-
29
- const schema: Record<string, unknown> = {
30
- $schema: "https://json-schema.org/draft/2020-12/schema",
31
- type: "object",
32
- required: [
33
- "schema_version",
34
- "product_name",
35
- "problem",
36
- "audience",
37
- "goals",
38
- "core_features",
39
- "constraints",
40
- "assumptions",
41
- "contracts"
42
- ],
43
- properties: {
44
- schema_version: { type: "string", minLength: 1 },
45
- product_name: { type: "string", minLength: 2 },
46
- problem: { type: "string", minLength: 10 },
47
- audience: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
48
- goals: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
49
- core_features: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
50
- constraints: { type: "array", items: { type: "string", minLength: 2 } },
51
- assumptions: { type: "array", items: { type: "string", minLength: 2 } },
52
- contracts: {
53
- type: "object",
54
- required: ["goals", "core_features", "constraints"],
55
- properties: {
56
- goals: { $ref: "#/$defs/contractArray" },
57
- core_features: { $ref: "#/$defs/contractArray" },
58
- constraints: { $ref: "#/$defs/contractArray" }
59
- },
60
- additionalProperties: false
61
- },
62
- confidence: {
63
- type: "object",
64
- additionalProperties: { type: "number", minimum: 0, maximum: 1 }
65
- }
66
- },
67
- $defs: {
68
- contractItem: {
69
- type: "object",
70
- required: ["id", "text"],
71
- properties: {
72
- id: { type: "string", pattern: "^[A-Z]+[0-9]+$" },
73
- text: { type: "string", minLength: 2 }
74
- },
75
- additionalProperties: false
76
- },
77
- contractArray: { type: "array", items: { $ref: "#/$defs/contractItem" } }
78
- },
79
- additionalProperties: false
80
- };
81
-
82
- const ajv = new Ajv2020({ allErrors: true, strict: false });
83
- const validateFn = ajv.compile(schema);
84
-
85
- function asString(value: unknown): string | undefined {
86
- if (typeof value !== "string") return undefined;
87
- const trimmed = value.trim();
88
- return trimmed.length > 0 ? trimmed : undefined;
89
- }
90
-
91
- function asStringArray(value: unknown): string[] {
92
- if (!Array.isArray(value)) return [];
93
- return value
94
- .map((item) => asString(item))
95
- .filter((item): item is string => typeof item === "string");
96
- }
97
-
98
- function asContracts(value: unknown): BriefContractItem[] {
99
- if (!Array.isArray(value)) return [];
100
- return value
101
- .map((item) => {
102
- if (!item || typeof item !== "object") return null;
103
- const record = item as Record<string, unknown>;
104
- const id = asString(record.id);
105
- const text = asString(record.text);
106
- if (!id || !text) return null;
107
- return { id, text };
108
- })
109
- .filter((item): item is BriefContractItem => item !== null);
110
- }
111
-
112
- function asConfidence(value: unknown): Record<string, number> | undefined {
113
- if (!value || typeof value !== "object") return undefined;
114
- const result: Record<string, number> = {};
115
- for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
116
- if (typeof raw !== "number" || Number.isNaN(raw)) continue;
117
- result[key] = raw;
118
- }
119
- return Object.keys(result).length > 0 ? result : undefined;
120
- }
121
-
122
- function normalizeInput(input: Record<string, unknown>): NormalizedBrief {
123
- const rawContracts = (input.contracts as Record<string, unknown> | undefined) ?? {};
124
- return {
125
- schema_version: asString(input.schema_version) ?? "1.0",
126
- product_name: asString(input.product_name) ?? "",
127
- problem: asString(input.problem) ?? "",
128
- audience: asStringArray(input.audience),
129
- goals: asStringArray(input.goals),
130
- core_features: asStringArray(input.core_features),
131
- constraints: asStringArray(input.constraints),
132
- assumptions: asStringArray(input.assumptions),
133
- contracts: {
134
- goals: asContracts(rawContracts.goals),
135
- core_features: asContracts(rawContracts.core_features),
136
- constraints: asContracts(rawContracts.constraints)
137
- },
138
- confidence: asConfidence(input.confidence)
139
- };
140
- }
141
-
142
- export function buildContractsFromArrays(input: {
143
- goals: string[];
144
- core_features: string[];
145
- constraints: string[];
146
- }): BriefContracts {
147
- return {
148
- goals: input.goals.map((text, index) => ({ id: `G${index + 1}`, text })),
149
- core_features: input.core_features.map((text, index) => ({ id: `F${index + 1}`, text })),
150
- constraints: input.constraints.map((text, index) => ({ id: `C${index + 1}`, text }))
151
- };
152
- }
153
-
154
- export function parseNormalizedBrief(input: Record<string, unknown>): {
155
- brief: NormalizedBrief;
156
- issues: ValidationIssue[];
157
- } {
158
- const normalized = normalizeInput(input);
159
- const valid = validateFn(normalized);
160
- if (valid) return { brief: normalized, issues: [] };
161
- const errors = validateFn.errors ?? [];
162
- const issues = errors.map((error) => ({
163
- level: "error" as const,
164
- code: "normalized_brief_invalid",
165
- check: "schema" as const,
166
- field: error.instancePath || error.schemaPath,
167
- message: `Normalized brief schema error: ${error.message ?? "unknown error"}`,
168
- suggestion: "Fix missing content in start brief and rerun `prodo normalize`."
169
- }));
170
- return { brief: normalized, issues };
171
- }
172
-
173
- export function parseNormalizedBriefOrThrow(input: Record<string, unknown>): NormalizedBrief {
174
- const { brief, issues } = parseNormalizedBrief(input);
175
- if (issues.length > 0) {
176
- const detail = issues.map((issue) => `- ${issue.message}`).join("\n");
177
- throw new UserError(`Normalized brief is invalid:\n${detail}`);
178
- }
179
- return brief;
180
- }
181
-
182
- export function requireConfidenceOrThrow(
183
- brief: NormalizedBrief,
184
- fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>,
185
- threshold = 0.7
186
- ): void {
187
- const confidence = brief.confidence ?? {};
188
- const missing = fields.filter((field) => (confidence[field] ?? 0) < threshold);
189
- if (missing.length > 0) {
190
- throw new UserError(
191
- `Normalization confidence too low for: ${missing.join(", ")}. Improve brief clarity and rerun \`prodo normalize\`.`
192
- );
193
- }
194
- }
195
-
196
- export function contractIds(contracts: BriefContracts): {
197
- goals: string[];
198
- core_features: string[];
199
- constraints: string[];
200
- } {
201
- return {
202
- goals: contracts.goals.map((item) => item.id),
203
- core_features: contracts.core_features.map((item) => item.id),
204
- constraints: contracts.constraints.map((item) => item.id)
205
- };
206
- }
1
+ import Ajv2020 from "ajv/dist/2020";
2
+ import { UserError } from "./errors";
3
+ import type { ValidationIssue } from "./types";
4
+
5
+ export type BriefContractItem = {
6
+ id: string;
7
+ text: string;
8
+ };
9
+
10
+ export type NormalizedBrief = {
11
+ schema_version: string;
12
+ product_name: string;
13
+ problem: string;
14
+ audience: string[];
15
+ goals: string[];
16
+ core_features: string[];
17
+ constraints: string[];
18
+ assumptions: string[];
19
+ contracts: {
20
+ goals: BriefContractItem[];
21
+ core_features: BriefContractItem[];
22
+ constraints: BriefContractItem[];
23
+ };
24
+ confidence?: Record<string, number>;
25
+ };
26
+
27
+ type BriefContracts = NormalizedBrief["contracts"];
28
+
29
+ const schema: Record<string, unknown> = {
30
+ $schema: "https://json-schema.org/draft/2020-12/schema",
31
+ type: "object",
32
+ required: [
33
+ "schema_version",
34
+ "product_name",
35
+ "problem",
36
+ "audience",
37
+ "goals",
38
+ "core_features",
39
+ "constraints",
40
+ "assumptions",
41
+ "contracts"
42
+ ],
43
+ properties: {
44
+ schema_version: { type: "string", minLength: 1 },
45
+ product_name: { type: "string", minLength: 2 },
46
+ problem: { type: "string", minLength: 10 },
47
+ audience: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
48
+ goals: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
49
+ core_features: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
50
+ constraints: { type: "array", items: { type: "string", minLength: 2 } },
51
+ assumptions: { type: "array", items: { type: "string", minLength: 2 } },
52
+ contracts: {
53
+ type: "object",
54
+ required: ["goals", "core_features", "constraints"],
55
+ properties: {
56
+ goals: { $ref: "#/$defs/contractArray" },
57
+ core_features: { $ref: "#/$defs/contractArray" },
58
+ constraints: { $ref: "#/$defs/contractArray" }
59
+ },
60
+ additionalProperties: false
61
+ },
62
+ confidence: {
63
+ type: "object",
64
+ additionalProperties: { type: "number", minimum: 0, maximum: 1 }
65
+ }
66
+ },
67
+ $defs: {
68
+ contractItem: {
69
+ type: "object",
70
+ required: ["id", "text"],
71
+ properties: {
72
+ id: { type: "string", pattern: "^[A-Z]+[0-9]+$" },
73
+ text: { type: "string", minLength: 2 }
74
+ },
75
+ additionalProperties: false
76
+ },
77
+ contractArray: { type: "array", items: { $ref: "#/$defs/contractItem" } }
78
+ },
79
+ additionalProperties: false
80
+ };
81
+
82
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
83
+ const validateFn = ajv.compile(schema);
84
+
85
+ function asString(value: unknown): string | undefined {
86
+ if (typeof value !== "string") return undefined;
87
+ const trimmed = value.trim();
88
+ return trimmed.length > 0 ? trimmed : undefined;
89
+ }
90
+
91
+ function asStringArray(value: unknown): string[] {
92
+ if (!Array.isArray(value)) return [];
93
+ return value
94
+ .map((item) => asString(item))
95
+ .filter((item): item is string => typeof item === "string");
96
+ }
97
+
98
+ function asContracts(value: unknown): BriefContractItem[] {
99
+ if (!Array.isArray(value)) return [];
100
+ return value
101
+ .map((item) => {
102
+ if (!item || typeof item !== "object") return null;
103
+ const record = item as Record<string, unknown>;
104
+ const id = asString(record.id);
105
+ const text = asString(record.text);
106
+ if (!id || !text) return null;
107
+ return { id, text };
108
+ })
109
+ .filter((item): item is BriefContractItem => item !== null);
110
+ }
111
+
112
+ function asConfidence(value: unknown): Record<string, number> | undefined {
113
+ if (!value || typeof value !== "object") return undefined;
114
+ const result: Record<string, number> = {};
115
+ for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
116
+ if (typeof raw !== "number" || Number.isNaN(raw)) continue;
117
+ result[key] = raw;
118
+ }
119
+ return Object.keys(result).length > 0 ? result : undefined;
120
+ }
121
+
122
+ function normalizeInput(input: Record<string, unknown>): NormalizedBrief {
123
+ const rawContracts = (input.contracts as Record<string, unknown> | undefined) ?? {};
124
+ return {
125
+ schema_version: asString(input.schema_version) ?? "1.0",
126
+ product_name: asString(input.product_name) ?? "",
127
+ problem: asString(input.problem) ?? "",
128
+ audience: asStringArray(input.audience),
129
+ goals: asStringArray(input.goals),
130
+ core_features: asStringArray(input.core_features),
131
+ constraints: asStringArray(input.constraints),
132
+ assumptions: asStringArray(input.assumptions),
133
+ contracts: {
134
+ goals: asContracts(rawContracts.goals),
135
+ core_features: asContracts(rawContracts.core_features),
136
+ constraints: asContracts(rawContracts.constraints)
137
+ },
138
+ confidence: asConfidence(input.confidence)
139
+ };
140
+ }
141
+
142
+ export function buildContractsFromArrays(input: {
143
+ goals: string[];
144
+ core_features: string[];
145
+ constraints: string[];
146
+ }): BriefContracts {
147
+ return {
148
+ goals: input.goals.map((text, index) => ({ id: `G${index + 1}`, text })),
149
+ core_features: input.core_features.map((text, index) => ({ id: `F${index + 1}`, text })),
150
+ constraints: input.constraints.map((text, index) => ({ id: `C${index + 1}`, text }))
151
+ };
152
+ }
153
+
154
+ export function parseNormalizedBrief(input: Record<string, unknown>): {
155
+ brief: NormalizedBrief;
156
+ issues: ValidationIssue[];
157
+ } {
158
+ const normalized = normalizeInput(input);
159
+ const valid = validateFn(normalized);
160
+ if (valid) return { brief: normalized, issues: [] };
161
+ const errors = validateFn.errors ?? [];
162
+ const issues = errors.map((error) => ({
163
+ level: "error" as const,
164
+ code: "normalized_brief_invalid",
165
+ check: "schema" as const,
166
+ field: error.instancePath || error.schemaPath,
167
+ message: `Normalized brief schema error: ${error.message ?? "unknown error"}`,
168
+ suggestion: "Fix missing content in start brief and rerun `prodo normalize`."
169
+ }));
170
+ return { brief: normalized, issues };
171
+ }
172
+
173
+ export function parseNormalizedBriefOrThrow(input: Record<string, unknown>): NormalizedBrief {
174
+ const { brief, issues } = parseNormalizedBrief(input);
175
+ if (issues.length > 0) {
176
+ const detail = issues.map((issue) => `- ${issue.message}`).join("\n");
177
+ throw new UserError(`Normalized brief is invalid:\n${detail}`);
178
+ }
179
+ return brief;
180
+ }
181
+
182
+ export function requireConfidenceOrThrow(
183
+ brief: NormalizedBrief,
184
+ fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>,
185
+ threshold = 0.7
186
+ ): void {
187
+ const confidence = brief.confidence ?? {};
188
+ const missing = fields.filter((field) => (confidence[field] ?? 0) < threshold);
189
+ if (missing.length > 0) {
190
+ throw new UserError(
191
+ `Normalization confidence too low for: ${missing.join(", ")}. Improve brief clarity and rerun \`prodo normalize\`.`
192
+ );
193
+ }
194
+ }
195
+
196
+ export type ConfidenceCheckResult = {
197
+ pass: boolean;
198
+ lowFields: Array<{ field: string; confidence: number; threshold: number }>;
199
+ };
200
+
201
+ export function checkConfidence(
202
+ brief: NormalizedBrief,
203
+ fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>,
204
+ threshold = 0.7
205
+ ): ConfidenceCheckResult {
206
+ const confidence = brief.confidence ?? {};
207
+ const lowFields = fields
208
+ .filter((field) => (confidence[field] ?? 0) < threshold)
209
+ .map((field) => ({
210
+ field,
211
+ confidence: confidence[field] ?? 0,
212
+ threshold
213
+ }));
214
+ return { pass: lowFields.length === 0, lowFields };
215
+ }
216
+
217
+ export function contractIds(contracts: BriefContracts): {
218
+ goals: string[];
219
+ core_features: string[];
220
+ constraints: string[];
221
+ } {
222
+ return {
223
+ goals: contracts.goals.map((item) => item.id),
224
+ core_features: contracts.core_features.map((item) => item.id),
225
+ constraints: contracts.constraints.map((item) => item.id)
226
+ };
227
+ }