@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,188 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveFixTargets = resolveFixTargets;
7
+ exports.buildFixProposal = buildFixProposal;
8
+ exports.createBackup = createBackup;
9
+ exports.restoreBackup = restoreBackup;
10
+ exports.applyFix = applyFix;
11
+ exports.runFix = runFix;
12
+ const promises_1 = __importDefault(require("node:fs/promises"));
13
+ const node_path_1 = __importDefault(require("node:path"));
14
+ const artifact_registry_1 = require("./artifact-registry");
15
+ const artifacts_1 = require("./artifacts");
16
+ const errors_1 = require("./errors");
17
+ const hook_executor_1 = require("./hook-executor");
18
+ const normalize_1 = require("./normalize");
19
+ const output_index_1 = require("./output-index");
20
+ const paths_1 = require("./paths");
21
+ const utils_1 = require("./utils");
22
+ const validate_1 = require("./validate");
23
+ async function resolveFixTargets(cwd, artifactTypes, issues) {
24
+ const direct = new Set(issues
25
+ .map((issue) => issue.artifactType)
26
+ .filter((artifactType) => typeof artifactType === "string" && artifactTypes.includes(artifactType)));
27
+ if (direct.size === 0)
28
+ return artifactTypes;
29
+ const defs = await (0, artifact_registry_1.listArtifactDefinitions)(cwd);
30
+ let changed = true;
31
+ while (changed) {
32
+ changed = false;
33
+ for (const def of defs) {
34
+ const needsRefresh = def.upstream.some((upstream) => direct.has(upstream));
35
+ if (needsRefresh && !direct.has(def.name)) {
36
+ direct.add(def.name);
37
+ changed = true;
38
+ }
39
+ }
40
+ }
41
+ return artifactTypes.filter((artifactType) => direct.has(artifactType));
42
+ }
43
+ async function buildFixProposal(options) {
44
+ const { cwd, strict, report } = options;
45
+ const initialReport = await (0, validate_1.runValidate)(cwd, {
46
+ strict: Boolean(strict),
47
+ report
48
+ });
49
+ if (initialReport.pass) {
50
+ return {
51
+ targets: [],
52
+ issues: [],
53
+ issuesByArtifact: new Map(),
54
+ initialReport
55
+ };
56
+ }
57
+ const artifactTypes = await (0, artifact_registry_1.listArtifactTypes)(cwd);
58
+ const targets = await resolveFixTargets(cwd, artifactTypes, initialReport.issues);
59
+ const issuesByArtifact = new Map();
60
+ for (const issue of initialReport.issues) {
61
+ if (issue.artifactType) {
62
+ const existing = issuesByArtifact.get(issue.artifactType) ?? [];
63
+ existing.push(issue);
64
+ issuesByArtifact.set(issue.artifactType, existing);
65
+ }
66
+ }
67
+ return {
68
+ targets,
69
+ issues: initialReport.issues,
70
+ issuesByArtifact,
71
+ initialReport
72
+ };
73
+ }
74
+ function backupBasePath(cwd) {
75
+ return node_path_1.default.join((0, paths_1.prodoPath)(cwd), "state", "backups");
76
+ }
77
+ async function createBackup(cwd, targets) {
78
+ const slug = (0, utils_1.timestampSlug)();
79
+ const backupDir = node_path_1.default.join(backupBasePath(cwd), slug);
80
+ await (0, utils_1.ensureDir)(backupDir);
81
+ const manifest = [];
82
+ for (const type of targets) {
83
+ const activePath = await (0, output_index_1.getActiveArtifactPath)(cwd, type);
84
+ if (!activePath || !(await (0, utils_1.fileExists)(activePath)))
85
+ continue;
86
+ const relPath = node_path_1.default.relative(cwd, activePath);
87
+ const destPath = node_path_1.default.join(backupDir, relPath);
88
+ await (0, utils_1.ensureDir)(node_path_1.default.dirname(destPath));
89
+ await promises_1.default.copyFile(activePath, destPath);
90
+ manifest.push({ type, source: activePath, dest: destPath });
91
+ const parsed = node_path_1.default.parse(activePath);
92
+ const sidecar = node_path_1.default.join(parsed.dir, `${parsed.name}.artifact.json`);
93
+ if (await (0, utils_1.fileExists)(sidecar)) {
94
+ const sidecarRel = node_path_1.default.relative(cwd, sidecar);
95
+ const sidecarDest = node_path_1.default.join(backupDir, sidecarRel);
96
+ await promises_1.default.copyFile(sidecar, sidecarDest);
97
+ }
98
+ const companionExtensions = type === "workflow" ? [".mmd"] : type === "wireframe" ? [".html"] : [];
99
+ for (const ext of companionExtensions) {
100
+ const companionPath = node_path_1.default.join(parsed.dir, `${parsed.name}${ext}`);
101
+ if (await (0, utils_1.fileExists)(companionPath)) {
102
+ const companionRel = node_path_1.default.relative(cwd, companionPath);
103
+ const companionDest = node_path_1.default.join(backupDir, companionRel);
104
+ await (0, utils_1.ensureDir)(node_path_1.default.dirname(companionDest));
105
+ await promises_1.default.copyFile(companionPath, companionDest);
106
+ }
107
+ }
108
+ }
109
+ await promises_1.default.writeFile(node_path_1.default.join(backupDir, "_manifest.json"), `${JSON.stringify({ timestamp: slug, targets, files: manifest }, null, 2)}\n`, "utf8");
110
+ return backupDir;
111
+ }
112
+ async function restoreBackup(cwd, backupDir) {
113
+ const manifestPath = node_path_1.default.join(backupDir, "_manifest.json");
114
+ if (!(await (0, utils_1.fileExists)(manifestPath))) {
115
+ throw new errors_1.UserError("Backup manifest not found. Cannot restore.");
116
+ }
117
+ const manifest = JSON.parse(await promises_1.default.readFile(manifestPath, "utf8"));
118
+ for (const entry of manifest.files) {
119
+ if (await (0, utils_1.fileExists)(entry.dest)) {
120
+ await (0, utils_1.ensureDir)(node_path_1.default.dirname(entry.source));
121
+ await promises_1.default.copyFile(entry.dest, entry.source);
122
+ }
123
+ }
124
+ }
125
+ async function applyFix(cwd, proposal, options) {
126
+ const { agent, strict, report, log = console.log } = options;
127
+ const backupDir = await createBackup(cwd, proposal.targets);
128
+ await (0, hook_executor_1.runHookPhase)(cwd, "before_normalize", log);
129
+ const normalizedPath = await (0, normalize_1.runNormalize)({ cwd });
130
+ log(`Normalized brief refreshed: ${normalizedPath}`);
131
+ await (0, hook_executor_1.runHookPhase)(cwd, "after_normalize", log);
132
+ for (const type of proposal.targets) {
133
+ await (0, hook_executor_1.runHookPhase)(cwd, `before_${type}`, log);
134
+ const agentResolved = agent ?? undefined;
135
+ const file = await (0, artifacts_1.generateArtifact)({
136
+ artifactType: type,
137
+ cwd,
138
+ normalizedBriefOverride: normalizedPath,
139
+ agent: agentResolved,
140
+ revisionType: "fix"
141
+ });
142
+ log(`${type.toUpperCase()} regenerated (fix): ${file}`);
143
+ await (0, hook_executor_1.runHookPhase)(cwd, `after_${type}`, log);
144
+ }
145
+ await (0, hook_executor_1.runHookPhase)(cwd, "before_validate", log);
146
+ const finalResult = await (0, validate_1.runValidate)(cwd, {
147
+ strict: Boolean(strict),
148
+ report
149
+ });
150
+ await (0, hook_executor_1.runHookPhase)(cwd, "after_validate", log);
151
+ return {
152
+ proposal,
153
+ applied: true,
154
+ finalPass: finalResult.pass,
155
+ reportPath: finalResult.reportPath,
156
+ backupDir
157
+ };
158
+ }
159
+ async function runFix(options) {
160
+ const { cwd, dryRun, log = console.log } = options;
161
+ const proposal = await buildFixProposal(options);
162
+ if (proposal.targets.length === 0) {
163
+ log("No blocking issues found. Nothing to fix.");
164
+ return {
165
+ proposal,
166
+ applied: false,
167
+ finalPass: true,
168
+ reportPath: proposal.initialReport.reportPath
169
+ };
170
+ }
171
+ if (dryRun) {
172
+ log(`[Dry Run] Would regenerate: ${proposal.targets.join(", ")}`);
173
+ log(`[Dry Run] ${proposal.issues.length} issue(s) identified.`);
174
+ for (const [type, issues] of proposal.issuesByArtifact) {
175
+ log(` ${type}: ${issues.length} issue(s)`);
176
+ for (const issue of issues) {
177
+ log(` - [${issue.level}] ${issue.code}: ${issue.message}`);
178
+ }
179
+ }
180
+ return {
181
+ proposal,
182
+ applied: false,
183
+ finalPass: false,
184
+ reportPath: proposal.initialReport.reportPath
185
+ };
186
+ }
187
+ return applyFix(cwd, proposal, options);
188
+ }
@@ -0,0 +1 @@
1
+ export declare function runHookPhase(cwd: string, phaseKey: string, log: (message: string) => void): Promise<void>;
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runHookPhase = runHookPhase;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_child_process_1 = require("node:child_process");
10
+ const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ const errors_1 = require("./errors");
12
+ const utils_1 = require("./utils");
13
+ function hooksPath(cwd) {
14
+ return node_path_1.default.join(cwd, ".prodo", "hooks.yml");
15
+ }
16
+ async function runShellCommand(command, cwd, timeoutMs) {
17
+ const parsed = parseCommand(command);
18
+ if (!parsed) {
19
+ return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
20
+ }
21
+ return new Promise((resolve) => {
22
+ const child = (0, node_child_process_1.spawn)(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
23
+ let stdout = "";
24
+ let stderr = "";
25
+ let timedOut = false;
26
+ const timer = setTimeout(() => {
27
+ timedOut = true;
28
+ child.kill();
29
+ }, Math.max(1000, timeoutMs));
30
+ child.stdout.on("data", (chunk) => {
31
+ stdout += chunk.toString();
32
+ });
33
+ child.stderr.on("data", (chunk) => {
34
+ stderr += chunk.toString();
35
+ });
36
+ child.on("close", (code) => {
37
+ clearTimeout(timer);
38
+ resolve({ code: code ?? 1, stdout, stderr, timedOut });
39
+ });
40
+ });
41
+ }
42
+ function parseCommand(command) {
43
+ const src = command.trim();
44
+ if (!src)
45
+ return null;
46
+ const out = [];
47
+ let current = "";
48
+ let quote = null;
49
+ let escaping = false;
50
+ for (let i = 0; i < src.length; i += 1) {
51
+ const ch = src[i];
52
+ if (escaping) {
53
+ current += ch;
54
+ escaping = false;
55
+ continue;
56
+ }
57
+ if (ch === "\\") {
58
+ escaping = true;
59
+ continue;
60
+ }
61
+ if (quote) {
62
+ if (ch === quote) {
63
+ quote = null;
64
+ }
65
+ else {
66
+ current += ch;
67
+ }
68
+ continue;
69
+ }
70
+ if (ch === "'" || ch === '"') {
71
+ quote = ch;
72
+ continue;
73
+ }
74
+ if (/\s/.test(ch)) {
75
+ if (current.length > 0) {
76
+ out.push(current);
77
+ current = "";
78
+ }
79
+ continue;
80
+ }
81
+ current += ch;
82
+ }
83
+ if (escaping || quote)
84
+ return null;
85
+ if (current.length > 0)
86
+ out.push(current);
87
+ if (out.length === 0)
88
+ return null;
89
+ return { bin: out[0], args: out.slice(1) };
90
+ }
91
+ function toPositiveInt(value, fallback) {
92
+ if (typeof value !== "number" || !Number.isFinite(value))
93
+ return fallback;
94
+ const normalized = Math.floor(value);
95
+ return normalized > 0 ? normalized : fallback;
96
+ }
97
+ function toNonNegativeInt(value, fallback) {
98
+ if (typeof value !== "number" || !Number.isFinite(value))
99
+ return fallback;
100
+ const normalized = Math.floor(value);
101
+ return normalized >= 0 ? normalized : fallback;
102
+ }
103
+ async function sleep(ms) {
104
+ await new Promise((resolve) => setTimeout(resolve, ms));
105
+ }
106
+ async function evaluateCondition(condition, cwd) {
107
+ const trimmed = condition.trim();
108
+ if (!trimmed)
109
+ return true;
110
+ const result = await runShellCommand(trimmed, cwd, 10_000);
111
+ return !result.timedOut && result.code === 0;
112
+ }
113
+ async function readHooks(cwd) {
114
+ const file = hooksPath(cwd);
115
+ if (!(await (0, utils_1.fileExists)(file)))
116
+ return null;
117
+ try {
118
+ const raw = await promises_1.default.readFile(file, "utf8");
119
+ const parsed = js_yaml_1.default.load(raw);
120
+ return parsed ?? null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ async function runHookPhase(cwd, phaseKey, log) {
127
+ const config = await readHooks(cwd);
128
+ const phaseHooks = config?.hooks?.[phaseKey];
129
+ if (!Array.isArray(phaseHooks) || phaseHooks.length === 0)
130
+ return;
131
+ for (const hook of phaseHooks) {
132
+ if (hook?.enabled === false)
133
+ continue;
134
+ const command = typeof hook?.command === "string" ? hook.command.trim() : "";
135
+ if (!command)
136
+ continue;
137
+ if (typeof hook.condition === "string" && hook.condition.trim()) {
138
+ const pass = await evaluateCondition(hook.condition, cwd);
139
+ if (!pass) {
140
+ log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
141
+ continue;
142
+ }
143
+ }
144
+ const label = hook.extension || hook.description || command;
145
+ if (hook.optional) {
146
+ log(`[Hook:optional:${phaseKey}] ${label}`);
147
+ if (hook.prompt)
148
+ log(` Prompt: ${hook.prompt}`);
149
+ log(` To run manually: ${command}`);
150
+ continue;
151
+ }
152
+ const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
153
+ const retries = toNonNegativeInt(hook.retry, 0);
154
+ const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
155
+ const attempts = 1 + retries;
156
+ let lastDetail = "";
157
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
158
+ log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
159
+ const result = await runShellCommand(command, cwd, timeoutMs);
160
+ if (!result.timedOut && result.code === 0) {
161
+ lastDetail = "";
162
+ break;
163
+ }
164
+ const stderr = result.stderr.trim();
165
+ const stdout = result.stdout.trim();
166
+ lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
167
+ if (attempt < attempts) {
168
+ await sleep(retryDelayMs);
169
+ }
170
+ }
171
+ if (lastDetail) {
172
+ throw new errors_1.UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,16 @@
1
+ export type MarkdownSection = {
2
+ heading: string;
3
+ headingKey: string;
4
+ level: number;
5
+ textLines: string[];
6
+ listItems: string[];
7
+ };
8
+ export declare function normalizeHeadingKey(heading: string): string;
9
+ export declare function parseMarkdownSections(markdown: string): MarkdownSection[];
10
+ export declare function extractRequiredHeadings(content: string): string[];
11
+ export type TaggedLine = {
12
+ contractId: string;
13
+ line: string;
14
+ };
15
+ export declare function taggedLinesByContract(body: string): TaggedLine[];
16
+ export declare function sectionTextMap(content: string): Map<string, string>;
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeHeadingKey = normalizeHeadingKey;
4
+ exports.parseMarkdownSections = parseMarkdownSections;
5
+ exports.extractRequiredHeadings = extractRequiredHeadings;
6
+ exports.taggedLinesByContract = taggedLinesByContract;
7
+ exports.sectionTextMap = sectionTextMap;
8
+ function normalizeText(input) {
9
+ return input.trim().replace(/\s+/g, " ");
10
+ }
11
+ function normalizeHeadingKey(heading) {
12
+ return normalizeText(heading)
13
+ .toLowerCase()
14
+ .replace(/[^a-z0-9\s]/g, " ")
15
+ .replace(/\s+/g, " ")
16
+ .trim();
17
+ }
18
+ function parseMarkdownSections(markdown) {
19
+ const sections = [];
20
+ let current = null;
21
+ for (const rawLine of markdown.split(/\r?\n/)) {
22
+ const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
23
+ if (headingMatch) {
24
+ const title = normalizeText(headingMatch[2]);
25
+ current = {
26
+ heading: title,
27
+ headingKey: normalizeHeadingKey(title),
28
+ level: headingMatch[1].length,
29
+ textLines: [],
30
+ listItems: []
31
+ };
32
+ sections.push(current);
33
+ continue;
34
+ }
35
+ if (!current)
36
+ continue;
37
+ const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
38
+ if (listMatch) {
39
+ const item = normalizeText(listMatch[1]);
40
+ if (item.length > 0 && !current.listItems.includes(item))
41
+ current.listItems.push(item);
42
+ if (item.length > 0)
43
+ current.textLines.push(item);
44
+ continue;
45
+ }
46
+ const text = normalizeText(rawLine);
47
+ if (text.length > 0)
48
+ current.textLines.push(text);
49
+ }
50
+ return sections;
51
+ }
52
+ function extractRequiredHeadings(content) {
53
+ const sections = parseMarkdownSections(content);
54
+ return sections
55
+ .filter((section) => section.level === 2)
56
+ .map((section) => `## ${section.heading}`)
57
+ .filter((heading) => heading.length > 3);
58
+ }
59
+ function taggedLinesByContract(body) {
60
+ const lines = body
61
+ .split(/\r?\n/)
62
+ .map((line) => line.trim())
63
+ .filter((line) => line.length > 0);
64
+ const tagged = [];
65
+ for (const line of lines) {
66
+ const matches = line.match(/\[([GFC][0-9]+)\]/g) ?? [];
67
+ for (const match of matches) {
68
+ tagged.push({ contractId: match.slice(1, -1), line });
69
+ }
70
+ }
71
+ return tagged;
72
+ }
73
+ function sectionTextMap(content) {
74
+ const sections = parseMarkdownSections(content);
75
+ const mapped = new Map();
76
+ for (const section of sections) {
77
+ const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
78
+ mapped.set(`## ${section.heading}`, parts.join("\n").trim());
79
+ }
80
+ return mapped;
81
+ }
@@ -0,0 +1,8 @@
1
+ type NormalizeOptions = {
2
+ cwd: string;
3
+ brief?: string;
4
+ out?: string;
5
+ additionalContext?: Record<string, string>;
6
+ };
7
+ export declare function runNormalize(options: NormalizeOptions): Promise<string>;
8
+ export {};
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.runNormalize = runNormalize;
7
+ const promises_1 = __importDefault(require("node:fs/promises"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const errors_1 = require("./errors");
10
+ const normalized_brief_1 = require("./normalized-brief");
11
+ const paths_1 = require("./paths");
12
+ const providers_1 = require("../providers");
13
+ const settings_1 = require("./settings");
14
+ const utils_1 = require("./utils");
15
+ function normalizedKey(value) {
16
+ return value
17
+ .normalize("NFD")
18
+ .replace(/[\u0300-\u036f]/g, "")
19
+ .replace(/ı/g, "i")
20
+ .replace(/İ/g, "I")
21
+ .toLowerCase()
22
+ .replace(/[^a-z0-9]+/g, " ")
23
+ .trim();
24
+ }
25
+ function extractBriefProductName(rawBrief) {
26
+ const lines = rawBrief.split(/\r?\n/);
27
+ for (let index = 0; index < lines.length; index += 1) {
28
+ const headingMatch = lines[index].match(/^\s*#{1,6}\s+(.+?)\s*$/);
29
+ if (!headingMatch)
30
+ continue;
31
+ const headingKey = normalizedKey(headingMatch[1]);
32
+ const isProductHeading = headingKey === "product name" ||
33
+ headingKey === "project name" ||
34
+ headingKey === "urun adi" ||
35
+ headingKey === "urun ismi";
36
+ if (!isProductHeading)
37
+ continue;
38
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
39
+ const rawLine = lines[cursor].trim();
40
+ if (!rawLine)
41
+ continue;
42
+ if (/^\s*#{1,6}\s+/.test(rawLine))
43
+ break;
44
+ const cleaned = rawLine.replace(/^\s*[-*]\s*/, "").trim();
45
+ if (cleaned.length > 0)
46
+ return cleaned;
47
+ }
48
+ }
49
+ return undefined;
50
+ }
51
+ function preserveOriginalProductName(parsed, rawBrief) {
52
+ const briefProductName = extractBriefProductName(rawBrief);
53
+ if (!briefProductName)
54
+ return parsed;
55
+ const generated = typeof parsed.product_name === "string" ? parsed.product_name : "";
56
+ if (!generated.trim())
57
+ return { ...parsed, product_name: briefProductName };
58
+ if (normalizedKey(generated) !== normalizedKey(briefProductName))
59
+ return parsed;
60
+ return { ...parsed, product_name: briefProductName };
61
+ }
62
+ function extractJsonObject(raw) {
63
+ const trimmed = raw.trim();
64
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
65
+ const candidate = fenced ? fenced[1] : trimmed;
66
+ try {
67
+ return JSON.parse(candidate);
68
+ }
69
+ catch {
70
+ throw new errors_1.UserError("Normalizer provider did not return valid JSON.");
71
+ }
72
+ }
73
+ async function runNormalize(options) {
74
+ const { cwd } = options;
75
+ const root = (0, paths_1.prodoPath)(cwd);
76
+ if (!(await (0, utils_1.fileExists)(root))) {
77
+ throw new errors_1.UserError("Missing .prodo directory. Run `prodo init .` first.");
78
+ }
79
+ const inPath = options.brief ? node_path_1.default.resolve(cwd, options.brief) : (0, paths_1.briefPath)(cwd);
80
+ if (!(await (0, utils_1.fileExists)(inPath))) {
81
+ throw new errors_1.UserError(`Brief file not found: ${inPath}`);
82
+ }
83
+ const rawBrief = await promises_1.default.readFile(inPath, "utf8");
84
+ const normalizePromptPath = node_path_1.default.join(root, "prompts", "normalize.md");
85
+ const normalizePrompt = await promises_1.default.readFile(normalizePromptPath, "utf8");
86
+ const settings = await (0, settings_1.readSettings)(cwd);
87
+ const provider = (0, providers_1.createProvider)();
88
+ const inputContext = {
89
+ briefMarkdown: rawBrief,
90
+ sourceBriefPath: inPath,
91
+ outputLanguage: settings.lang
92
+ };
93
+ if (options.additionalContext && Object.keys(options.additionalContext).length > 0) {
94
+ inputContext.userClarifications = options.additionalContext;
95
+ }
96
+ const generated = await provider.generate(normalizePrompt, inputContext, {
97
+ artifactType: "normalize",
98
+ requiredHeadings: [],
99
+ requiredContracts: []
100
+ });
101
+ const parsed = extractJsonObject(generated.body);
102
+ const preserved = preserveOriginalProductName(parsed, rawBrief);
103
+ const withContracts = {
104
+ ...preserved,
105
+ contracts: preserved.contracts ??
106
+ (0, normalized_brief_1.buildContractsFromArrays)({
107
+ goals: Array.isArray(preserved.goals) ? preserved.goals.filter((x) => typeof x === "string") : [],
108
+ core_features: Array.isArray(preserved.core_features)
109
+ ? preserved.core_features.filter((x) => typeof x === "string")
110
+ : [],
111
+ constraints: Array.isArray(preserved.constraints)
112
+ ? preserved.constraints.filter((x) => typeof x === "string")
113
+ : []
114
+ })
115
+ };
116
+ const normalized = (0, normalized_brief_1.parseNormalizedBriefOrThrow)(withContracts);
117
+ const outPath = options.out ? node_path_1.default.resolve(cwd, options.out) : (0, paths_1.normalizedBriefPath)(cwd);
118
+ if (!(0, utils_1.isPathInside)((0, paths_1.prodoPath)(cwd), outPath)) {
119
+ throw new errors_1.UserError("Normalize output must be inside `.prodo/`.");
120
+ }
121
+ await promises_1.default.mkdir(node_path_1.default.dirname(outPath), { recursive: true });
122
+ await promises_1.default.writeFile(outPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
123
+ (0, normalized_brief_1.requireConfidenceOrThrow)(normalized, ["product_name", "problem", "audience", "goals", "core_features"], 0.7);
124
+ return outPath;
125
+ }
@@ -0,0 +1,48 @@
1
+ import type { ValidationIssue } from "./types";
2
+ export type BriefContractItem = {
3
+ id: string;
4
+ text: string;
5
+ };
6
+ export type NormalizedBrief = {
7
+ schema_version: string;
8
+ product_name: string;
9
+ problem: string;
10
+ audience: string[];
11
+ goals: string[];
12
+ core_features: string[];
13
+ constraints: string[];
14
+ assumptions: string[];
15
+ contracts: {
16
+ goals: BriefContractItem[];
17
+ core_features: BriefContractItem[];
18
+ constraints: BriefContractItem[];
19
+ };
20
+ confidence?: Record<string, number>;
21
+ };
22
+ type BriefContracts = NormalizedBrief["contracts"];
23
+ export declare function buildContractsFromArrays(input: {
24
+ goals: string[];
25
+ core_features: string[];
26
+ constraints: string[];
27
+ }): BriefContracts;
28
+ export declare function parseNormalizedBrief(input: Record<string, unknown>): {
29
+ brief: NormalizedBrief;
30
+ issues: ValidationIssue[];
31
+ };
32
+ export declare function parseNormalizedBriefOrThrow(input: Record<string, unknown>): NormalizedBrief;
33
+ export declare function requireConfidenceOrThrow(brief: NormalizedBrief, fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>, threshold?: number): void;
34
+ export type ConfidenceCheckResult = {
35
+ pass: boolean;
36
+ lowFields: Array<{
37
+ field: string;
38
+ confidence: number;
39
+ threshold: number;
40
+ }>;
41
+ };
42
+ export declare function checkConfidence(brief: NormalizedBrief, fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>, threshold?: number): ConfidenceCheckResult;
43
+ export declare function contractIds(contracts: BriefContracts): {
44
+ goals: string[];
45
+ core_features: string[];
46
+ constraints: string[];
47
+ };
48
+ export {};