@shahmarasy/prodo 0.1.4 → 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 (173) 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 +465 -0
  27. package/dist/cli/init-tui.d.ts +23 -0
  28. package/dist/cli/init-tui.js +176 -0
  29. package/dist/cli/init.d.ts +11 -0
  30. package/dist/cli/init.js +334 -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 +7 -0
  66. package/dist/core/settings.js +35 -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 +7 -6
  97. package/dist/providers/openai-provider.d.ts +1 -1
  98. package/dist/providers/openai-provider.js +1 -1
  99. package/dist/skills/engine.d.ts +10 -0
  100. package/dist/skills/engine.js +75 -0
  101. package/dist/skills/fix-skill.d.ts +2 -0
  102. package/dist/skills/fix-skill.js +38 -0
  103. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  104. package/dist/skills/generate-artifact-skill.js +32 -0
  105. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  106. package/dist/skills/generate-pipeline-skill.js +45 -0
  107. package/dist/skills/normalize-skill.d.ts +2 -0
  108. package/dist/skills/normalize-skill.js +29 -0
  109. package/dist/skills/types.d.ts +28 -0
  110. package/dist/skills/types.js +2 -0
  111. package/dist/skills/validate-skill.d.ts +2 -0
  112. package/dist/skills/validate-skill.js +29 -0
  113. package/package.json +74 -45
  114. package/src/agents/agent-registry.ts +93 -0
  115. package/src/agents/anthropic/index.ts +86 -0
  116. package/src/agents/anthropic/manifest.json +7 -0
  117. package/src/agents/base.ts +77 -0
  118. package/src/agents/google/index.ts +79 -0
  119. package/src/agents/google/manifest.json +7 -0
  120. package/src/agents/mock/index.ts +32 -0
  121. package/src/agents/mock/manifest.json +7 -0
  122. package/src/agents/openai/index.ts +83 -0
  123. package/src/agents/openai/manifest.json +7 -0
  124. package/src/agents/system-prompts.ts +35 -0
  125. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  126. package/src/{agents.ts → cli/agent-ids.ts} +58 -58
  127. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  128. package/src/cli/fix-tui.ts +111 -0
  129. package/src/{cli.ts → cli/index.ts} +459 -410
  130. package/src/{init-tui.ts → cli/init-tui.ts} +208 -208
  131. package/src/{init.ts → cli/init.ts} +398 -398
  132. package/src/cli/normalize-interactive.ts +241 -0
  133. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  134. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  135. package/src/{artifacts.ts → core/artifacts.ts} +1081 -1072
  136. package/src/core/clean.ts +88 -0
  137. package/src/{consistency.ts → core/consistency.ts} +374 -303
  138. package/src/{constants.ts → core/constants.ts} +72 -72
  139. package/src/{errors.ts → core/errors.ts} +7 -7
  140. package/src/core/fix.ts +253 -0
  141. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  142. package/src/{markdown.ts → core/markdown.ts} +93 -73
  143. package/src/{normalize.ts → core/normalize.ts} +145 -137
  144. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  145. package/src/{output-index.ts → core/output-index.ts} +59 -59
  146. package/src/{paths.ts → core/paths.ts} +75 -71
  147. package/src/{project-config.ts → core/project-config.ts} +78 -78
  148. package/src/{registry.ts → core/registry.ts} +119 -119
  149. package/src/{settings.ts → core/settings.ts} +35 -35
  150. package/src/core/template-engine.ts +45 -0
  151. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  152. package/src/{templates.ts → core/templates.ts} +452 -452
  153. package/src/core/terminology.ts +177 -0
  154. package/src/core/tracing.ts +110 -0
  155. package/src/{types.ts → core/types.ts} +46 -46
  156. package/src/{utils.ts → core/utils.ts} +64 -64
  157. package/src/{validate.ts → core/validate.ts} +252 -246
  158. package/src/{validator.ts → core/validator.ts} +92 -92
  159. package/src/{version.ts → core/version.ts} +24 -24
  160. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
  161. package/src/i18n/en.json +45 -0
  162. package/src/i18n/index.ts +58 -0
  163. package/src/i18n/tr.json +45 -0
  164. package/src/providers/index.ts +29 -12
  165. package/src/providers/mock-provider.ts +200 -199
  166. package/src/providers/openai-provider.ts +88 -88
  167. package/src/skills/engine.ts +94 -0
  168. package/src/skills/fix-skill.ts +38 -0
  169. package/src/skills/generate-artifact-skill.ts +32 -0
  170. package/src/skills/generate-pipeline-skill.ts +49 -0
  171. package/src/skills/normalize-skill.ts +29 -0
  172. package/src/skills/types.ts +36 -0
  173. package/src/skills/validate-skill.ts +29 -0
@@ -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
+ }
@@ -1,59 +1,59 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { outputIndexPath } from "./paths";
4
- import type { ArtifactType } from "./types";
5
- import { ensureDir, fileExists } from "./utils";
6
-
7
- type ArtifactMap = Partial<Record<ArtifactType, string>>;
8
- type ArtifactHistoryMap = Partial<Record<ArtifactType, string[]>>;
9
-
10
- export type OutputIndex = {
11
- active: ArtifactMap;
12
- history: ArtifactHistoryMap;
13
- updated_at: string;
14
- };
15
-
16
- function defaultIndex(): OutputIndex {
17
- return {
18
- active: {},
19
- history: {},
20
- updated_at: new Date(0).toISOString()
21
- };
22
- }
23
-
24
- export async function loadOutputIndex(cwd: string): Promise<OutputIndex> {
25
- const indexPath = outputIndexPath(cwd);
26
- if (!(await fileExists(indexPath))) return defaultIndex();
27
- const raw = await fs.readFile(indexPath, "utf8");
28
- const parsed = JSON.parse(raw) as Partial<OutputIndex>;
29
- return {
30
- active: parsed.active ?? {},
31
- history: parsed.history ?? {},
32
- updated_at: parsed.updated_at ?? new Date(0).toISOString()
33
- };
34
- }
35
-
36
- export async function saveOutputIndex(cwd: string, index: OutputIndex): Promise<void> {
37
- const indexPath = outputIndexPath(cwd);
38
- await ensureDir(path.dirname(indexPath));
39
- await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
40
- }
41
-
42
- export async function setActiveArtifact(cwd: string, type: ArtifactType, filePath: string): Promise<void> {
43
- const index = await loadOutputIndex(cwd);
44
- const normalizedPath = path.resolve(filePath);
45
- const existing = index.history[type] ?? [];
46
- index.active[type] = normalizedPath;
47
- index.history[type] = [normalizedPath, ...existing.filter((item) => item !== normalizedPath)].slice(0, 100);
48
- index.updated_at = new Date().toISOString();
49
- await saveOutputIndex(cwd, index);
50
- }
51
-
52
- export async function getActiveArtifactPath(cwd: string, type: ArtifactType): Promise<string | undefined> {
53
- const index = await loadOutputIndex(cwd);
54
- const candidate = index.active[type];
55
- if (!candidate) return undefined;
56
- if (await fileExists(candidate)) return candidate;
57
- return undefined;
58
- }
59
-
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { outputIndexPath } from "./paths";
4
+ import type { ArtifactType } from "./types";
5
+ import { ensureDir, fileExists } from "./utils";
6
+
7
+ type ArtifactMap = Partial<Record<ArtifactType, string>>;
8
+ type ArtifactHistoryMap = Partial<Record<ArtifactType, string[]>>;
9
+
10
+ export type OutputIndex = {
11
+ active: ArtifactMap;
12
+ history: ArtifactHistoryMap;
13
+ updated_at: string;
14
+ };
15
+
16
+ function defaultIndex(): OutputIndex {
17
+ return {
18
+ active: {},
19
+ history: {},
20
+ updated_at: new Date(0).toISOString()
21
+ };
22
+ }
23
+
24
+ export async function loadOutputIndex(cwd: string): Promise<OutputIndex> {
25
+ const indexPath = outputIndexPath(cwd);
26
+ if (!(await fileExists(indexPath))) return defaultIndex();
27
+ const raw = await fs.readFile(indexPath, "utf8");
28
+ const parsed = JSON.parse(raw) as Partial<OutputIndex>;
29
+ return {
30
+ active: parsed.active ?? {},
31
+ history: parsed.history ?? {},
32
+ updated_at: parsed.updated_at ?? new Date(0).toISOString()
33
+ };
34
+ }
35
+
36
+ export async function saveOutputIndex(cwd: string, index: OutputIndex): Promise<void> {
37
+ const indexPath = outputIndexPath(cwd);
38
+ await ensureDir(path.dirname(indexPath));
39
+ await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
40
+ }
41
+
42
+ export async function setActiveArtifact(cwd: string, type: ArtifactType, filePath: string): Promise<void> {
43
+ const index = await loadOutputIndex(cwd);
44
+ const normalizedPath = path.resolve(filePath);
45
+ const existing = index.history[type] ?? [];
46
+ index.active[type] = normalizedPath;
47
+ index.history[type] = [normalizedPath, ...existing.filter((item) => item !== normalizedPath)].slice(0, 100);
48
+ index.updated_at = new Date().toISOString();
49
+ await saveOutputIndex(cwd, index);
50
+ }
51
+
52
+ export async function getActiveArtifactPath(cwd: string, type: ArtifactType): Promise<string | undefined> {
53
+ const index = await loadOutputIndex(cwd);
54
+ const candidate = index.active[type];
55
+ if (!candidate) return undefined;
56
+ if (await fileExists(candidate)) return candidate;
57
+ return undefined;
58
+ }
59
+