@shahmarasy/prodo 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents.js +4 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +265 -31
- package/dist/cli.js +80 -3
- package/dist/init-tui.d.ts +3 -0
- package/dist/init-tui.js +28 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +9 -3
- package/dist/normalize.js +55 -7
- package/dist/providers/openai-provider.js +2 -1
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/validator.js +0 -4
- package/dist/workflow-commands.js +2 -1
- package/package.json +1 -1
- package/presets/fintech/preset.json +48 -1
- package/presets/fintech/prompts/prd.md +99 -2
- package/presets/marketplace/preset.json +51 -1
- package/presets/marketplace/prompts/prd.md +140 -2
- package/presets/saas/preset.json +53 -1
- package/presets/saas/prompts/prd.md +150 -2
- package/src/agents.ts +4 -2
- package/src/artifacts.ts +323 -28
- package/src/cli.ts +97 -6
- package/src/init-tui.ts +30 -1
- package/src/init.ts +11 -4
- package/src/normalize.ts +55 -7
- package/src/providers/openai-provider.ts +2 -1
- package/src/settings.ts +3 -2
- package/src/templates.ts +2 -0
- package/src/utils.ts +14 -0
- package/src/validator.ts +0 -4
- package/src/workflow-commands.ts +2 -1
- package/templates/commands/prodo-fix.md +46 -0
- package/templates/commands/prodo-normalize.md +118 -23
- package/templates/commands/prodo-prd.md +138 -17
- package/templates/commands/prodo-stories.md +153 -17
- package/templates/commands/prodo-techspec.md +167 -17
- package/templates/commands/prodo-validate.md +184 -26
- package/templates/commands/prodo-wireframe.md +188 -17
- package/templates/commands/prodo-workflow.md +200 -17
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { loadAgentCommandSet, resolveAgent } from "./agents";
|
|
6
6
|
import { resolveAi, type SupportedAi } from "./agent-command-installer";
|
|
7
|
-
import { listArtifactTypes } from "./artifact-registry";
|
|
7
|
+
import { listArtifactDefinitions, listArtifactTypes } from "./artifact-registry";
|
|
8
8
|
import { generateArtifact } from "./artifacts";
|
|
9
9
|
import { runDoctor } from "./doctor";
|
|
10
10
|
import { UserError } from "./errors";
|
|
@@ -30,10 +30,11 @@ const dynamicImport = new Function("specifier", "return import(specifier)") as (
|
|
|
30
30
|
specifier: string
|
|
31
31
|
) => Promise<unknown>;
|
|
32
32
|
|
|
33
|
-
function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | undefined {
|
|
33
|
+
function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | "fix" | undefined {
|
|
34
34
|
if (forcedCommand === "prodo-init") return "init";
|
|
35
35
|
if (forcedCommand === "prodo-validate") return "validate";
|
|
36
36
|
if (forcedCommand === "prodo-normalize") return "normalize";
|
|
37
|
+
if (forcedCommand === "prodo-fix") return "fix";
|
|
37
38
|
if (forcedCommand === "prodo-prd") return "prd";
|
|
38
39
|
if (forcedCommand === "prodo-workflow") return "workflow";
|
|
39
40
|
if (forcedCommand === "prodo-wireframe") return "wireframe";
|
|
@@ -44,7 +45,7 @@ function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "valid
|
|
|
44
45
|
|
|
45
46
|
async function runArtifactCommand(
|
|
46
47
|
type: ArtifactType,
|
|
47
|
-
opts: { from?: string; out?: string; agent?: string },
|
|
48
|
+
opts: { from?: string; out?: string; agent?: string; revisionType?: "default" | "fix" },
|
|
48
49
|
cwd: string,
|
|
49
50
|
log: (message: string) => void,
|
|
50
51
|
options?: { suggestValidate?: boolean }
|
|
@@ -56,7 +57,8 @@ async function runArtifactCommand(
|
|
|
56
57
|
cwd,
|
|
57
58
|
normalizedBriefOverride: opts.from,
|
|
58
59
|
outPath: opts.out,
|
|
59
|
-
agent
|
|
60
|
+
agent,
|
|
61
|
+
revisionType: opts.revisionType
|
|
60
62
|
});
|
|
61
63
|
const agentMsg = agent ? ` [agent=${agent}]` : "";
|
|
62
64
|
log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
|
|
@@ -116,13 +118,15 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
116
118
|
.command("init [target]")
|
|
117
119
|
.option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
|
|
118
120
|
.option("--lang <code>", "document language (e.g. en, tr)")
|
|
121
|
+
.option("--author <name>", "document author name")
|
|
119
122
|
.option("--preset <name>", "preset to install during initialization")
|
|
120
123
|
.action(async (target, opts) => {
|
|
121
124
|
const projectRoot = path.resolve(cwd, target ?? ".");
|
|
122
125
|
const selected = await gatherInitSelections({
|
|
123
126
|
projectRoot,
|
|
124
127
|
aiInput: opts.ai,
|
|
125
|
-
langInput: opts.lang
|
|
128
|
+
langInput: opts.lang,
|
|
129
|
+
authorInput: opts.author
|
|
126
130
|
});
|
|
127
131
|
const selectedAi = selected.ai as SupportedAi | undefined;
|
|
128
132
|
|
|
@@ -133,6 +137,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
133
137
|
const result = await runInit(projectRoot, {
|
|
134
138
|
ai: selectedAi,
|
|
135
139
|
lang: selected.lang,
|
|
140
|
+
author: selected.author,
|
|
136
141
|
preset: opts.preset,
|
|
137
142
|
script: selected.script
|
|
138
143
|
});
|
|
@@ -141,7 +146,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
141
146
|
projectRoot,
|
|
142
147
|
settingsPath: result.settingsPath,
|
|
143
148
|
ai: selectedAi,
|
|
144
|
-
lang: selected.lang
|
|
149
|
+
lang: selected.lang,
|
|
150
|
+
author: selected.author
|
|
145
151
|
});
|
|
146
152
|
return;
|
|
147
153
|
}
|
|
@@ -149,6 +155,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
149
155
|
const result = await runInit(projectRoot, {
|
|
150
156
|
ai: selectedAi,
|
|
151
157
|
lang: selected.lang,
|
|
158
|
+
author: selected.author,
|
|
152
159
|
preset: opts.preset,
|
|
153
160
|
script: selected.script
|
|
154
161
|
});
|
|
@@ -161,6 +168,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
161
168
|
out("No agent selected. Use `prodo generate` for end-to-end generation.");
|
|
162
169
|
}
|
|
163
170
|
out(`Settings file: ${result.settingsPath}`);
|
|
171
|
+
out(`Author: ${selected.author}`);
|
|
164
172
|
out("Next: edit brief.md.");
|
|
165
173
|
});
|
|
166
174
|
|
|
@@ -198,6 +206,56 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
198
206
|
});
|
|
199
207
|
});
|
|
200
208
|
|
|
209
|
+
program
|
|
210
|
+
.command("fix", { hidden: true })
|
|
211
|
+
.description("Advanced: auto-regenerate affected artifacts from validation findings")
|
|
212
|
+
.option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
|
|
213
|
+
.option("--strict", "treat validation warnings as errors")
|
|
214
|
+
.option("--report <path>", "validation report output path")
|
|
215
|
+
.action(async (opts) => {
|
|
216
|
+
if (opts.agent) resolveAgent(opts.agent);
|
|
217
|
+
await withBriefReadOnlyGuard(cwd, async () => {
|
|
218
|
+
await runHookPhase(cwd, "before_validate", out);
|
|
219
|
+
const initial = await runValidate(cwd, {
|
|
220
|
+
strict: Boolean(opts.strict),
|
|
221
|
+
report: opts.report
|
|
222
|
+
});
|
|
223
|
+
out(`Validation report written to: ${initial.reportPath}`);
|
|
224
|
+
await runHookPhase(cwd, "after_validate", out);
|
|
225
|
+
|
|
226
|
+
if (initial.pass) {
|
|
227
|
+
out("No blocking issues found. Nothing to fix.");
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const targets = await resolveFixTargets(cwd, artifactTypes, initial.issues);
|
|
232
|
+
out(`Validation failed. Regenerating impacted artifacts: ${targets.join(", ")}`);
|
|
233
|
+
|
|
234
|
+
await runHookPhase(cwd, "before_normalize", out);
|
|
235
|
+
const normalizedPath = await runNormalize({ cwd });
|
|
236
|
+
out(`Normalized brief refreshed: ${normalizedPath}`);
|
|
237
|
+
await runHookPhase(cwd, "after_normalize", out);
|
|
238
|
+
|
|
239
|
+
for (const type of targets) {
|
|
240
|
+
await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent, revisionType: "fix" }, cwd, out, {
|
|
241
|
+
suggestValidate: false
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await runHookPhase(cwd, "before_validate", out);
|
|
246
|
+
const final = await runValidate(cwd, {
|
|
247
|
+
strict: Boolean(opts.strict),
|
|
248
|
+
report: opts.report
|
|
249
|
+
});
|
|
250
|
+
out(`Validation report written to: ${final.reportPath}`);
|
|
251
|
+
if (!final.pass) {
|
|
252
|
+
throw new UserError("Fix completed but validation is still failing. Review report and retry.");
|
|
253
|
+
}
|
|
254
|
+
out("Fix pipeline completed. Validation passed.");
|
|
255
|
+
await runHookPhase(cwd, "after_validate", out);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
201
259
|
program
|
|
202
260
|
.command("normalize", { hidden: true })
|
|
203
261
|
.description("Advanced: normalize brief without full pipeline")
|
|
@@ -294,6 +352,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
|
|
|
294
352
|
await program.parseAsync(["node", "prodo", "normalize", ...argv.slice(2)]);
|
|
295
353
|
} else if (forced === "validate") {
|
|
296
354
|
await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
|
|
355
|
+
} else if (forced === "fix") {
|
|
356
|
+
await program.parseAsync(["node", "prodo", "fix", ...argv.slice(2)]);
|
|
297
357
|
} else {
|
|
298
358
|
await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
|
|
299
359
|
}
|
|
@@ -317,3 +377,34 @@ if (require.main === module) {
|
|
|
317
377
|
process.exitCode = code;
|
|
318
378
|
});
|
|
319
379
|
}
|
|
380
|
+
|
|
381
|
+
async function resolveFixTargets(
|
|
382
|
+
cwd: string,
|
|
383
|
+
artifactTypes: ArtifactType[],
|
|
384
|
+
issues: Array<{ artifactType?: ArtifactType }>
|
|
385
|
+
): Promise<ArtifactType[]> {
|
|
386
|
+
const direct = new Set<ArtifactType>(
|
|
387
|
+
issues
|
|
388
|
+
.map((issue) => issue.artifactType)
|
|
389
|
+
.filter(
|
|
390
|
+
(artifactType): artifactType is ArtifactType =>
|
|
391
|
+
typeof artifactType === "string" && artifactTypes.includes(artifactType)
|
|
392
|
+
)
|
|
393
|
+
);
|
|
394
|
+
if (direct.size === 0) return artifactTypes;
|
|
395
|
+
|
|
396
|
+
const defs = await listArtifactDefinitions(cwd);
|
|
397
|
+
let changed = true;
|
|
398
|
+
while (changed) {
|
|
399
|
+
changed = false;
|
|
400
|
+
for (const def of defs) {
|
|
401
|
+
const needsRefresh = def.upstream.some((upstream) => direct.has(upstream));
|
|
402
|
+
if (needsRefresh && !direct.has(def.name)) {
|
|
403
|
+
direct.add(def.name);
|
|
404
|
+
changed = true;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return artifactTypes.filter((artifactType) => direct.has(artifactType));
|
|
410
|
+
}
|
package/src/init-tui.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
2
3
|
import { resolveAi, type SupportedAi } from "./agent-command-installer";
|
|
3
4
|
import { UserError } from "./errors";
|
|
4
5
|
import { fileExists } from "./utils";
|
|
@@ -7,6 +8,7 @@ export type InitSelections = {
|
|
|
7
8
|
ai?: SupportedAi;
|
|
8
9
|
script: "sh" | "ps";
|
|
9
10
|
lang: "tr" | "en";
|
|
11
|
+
author: string;
|
|
10
12
|
interactive: boolean;
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -14,6 +16,7 @@ type GatherInitUiOptions = {
|
|
|
14
16
|
projectRoot: string;
|
|
15
17
|
aiInput?: string;
|
|
16
18
|
langInput?: string;
|
|
19
|
+
authorInput?: string;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
22
|
type ClackPrompts = typeof import("@clack/prompts");
|
|
@@ -93,17 +96,31 @@ function modelChoiceFromAi(ai?: SupportedAi): "codex" | "gemini" | "auto-detect"
|
|
|
93
96
|
return "auto-detect";
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
function defaultAuthorName(authorInput?: string): string {
|
|
100
|
+
const explicit = (authorInput ?? "").trim();
|
|
101
|
+
if (explicit.length > 0) return explicit;
|
|
102
|
+
try {
|
|
103
|
+
const username = os.userInfo().username.trim();
|
|
104
|
+
if (username.length > 0) return username;
|
|
105
|
+
} catch {
|
|
106
|
+
// ignore lookup errors and continue with fallback
|
|
107
|
+
}
|
|
108
|
+
return "Product Author";
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
export async function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections> {
|
|
97
112
|
const clack = await loadClack();
|
|
98
113
|
const defaultLang = normalizeLang(options.langInput);
|
|
99
114
|
const fallbackScript: "sh" | "ps" = process.platform === "win32" ? "ps" : "sh";
|
|
100
115
|
const parsedAi = resolveAi(options.aiInput);
|
|
116
|
+
const defaultAuthor = defaultAuthorName(options.authorInput);
|
|
101
117
|
|
|
102
118
|
if (!isInteractiveTerminal()) {
|
|
103
119
|
return {
|
|
104
120
|
ai: parsedAi,
|
|
105
121
|
script: fallbackScript,
|
|
106
122
|
lang: defaultLang,
|
|
123
|
+
author: defaultAuthor,
|
|
107
124
|
interactive: false
|
|
108
125
|
};
|
|
109
126
|
}
|
|
@@ -153,6 +170,16 @@ export async function gatherInitSelections(options: GatherInitUiOptions): Promis
|
|
|
153
170
|
throw new UserError("Initialization cancelled.");
|
|
154
171
|
}
|
|
155
172
|
|
|
173
|
+
const author = await clack.text({
|
|
174
|
+
message: "Author name",
|
|
175
|
+
placeholder: "Shahmarasy",
|
|
176
|
+
defaultValue: defaultAuthor
|
|
177
|
+
});
|
|
178
|
+
if (clack.isCancel(author)) {
|
|
179
|
+
clack.cancel("Initialization cancelled.");
|
|
180
|
+
throw new UserError("Initialization cancelled.");
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
let selectedAi: SupportedAi | undefined;
|
|
157
184
|
if (model === "codex") selectedAi = "codex";
|
|
158
185
|
else if (model === "gemini") selectedAi = "gemini-cli";
|
|
@@ -162,6 +189,7 @@ export async function gatherInitSelections(options: GatherInitUiOptions): Promis
|
|
|
162
189
|
ai: selectedAi,
|
|
163
190
|
script: fallbackScript,
|
|
164
191
|
lang,
|
|
192
|
+
author: String(author).trim() || defaultAuthor,
|
|
165
193
|
interactive: true
|
|
166
194
|
};
|
|
167
195
|
}
|
|
@@ -171,9 +199,10 @@ export function finishInitInteractive(summary: {
|
|
|
171
199
|
settingsPath: string;
|
|
172
200
|
ai?: SupportedAi;
|
|
173
201
|
lang: "tr" | "en";
|
|
202
|
+
author: string;
|
|
174
203
|
}): Promise<void> {
|
|
175
204
|
const aiText = summary.ai ?? "none";
|
|
176
205
|
return loadClack().then((clack) => clack.outro(
|
|
177
|
-
`Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
|
|
206
|
+
`Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nAuthor: ${summary.author}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
|
|
178
207
|
));
|
|
179
208
|
}
|
package/src/init.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { briefPath, outputDirPath, outputIndexPath, prodoPath } from "./paths";
|
|
|
9
9
|
import { applyConfiguredPresets } from "./preset-loader";
|
|
10
10
|
import { syncRegistry } from "./registry";
|
|
11
11
|
import { writeSettings } from "./settings";
|
|
12
|
+
import { extractRequiredHeadingsFromTemplate } from "./template-resolver";
|
|
12
13
|
import { buildWorkflowCommands } from "./workflow-commands";
|
|
13
14
|
import {
|
|
14
15
|
NORMALIZED_BRIEF_TEMPLATE,
|
|
@@ -283,7 +284,7 @@ function summarizeParity(items: AssetManifestItem[]): ScaffoldManifest["parity_s
|
|
|
283
284
|
|
|
284
285
|
export async function runInit(
|
|
285
286
|
cwd: string,
|
|
286
|
-
options?: { ai?: SupportedAi; lang?: string; preset?: string; script?: "sh" | "ps" }
|
|
287
|
+
options?: { ai?: SupportedAi; lang?: string; author?: string; preset?: string; script?: "sh" | "ps" }
|
|
287
288
|
): Promise<{ installedAgentFiles: string[]; settingsPath: string }> {
|
|
288
289
|
const root = prodoPath(cwd);
|
|
289
290
|
const artifactDefs = await listArtifactDefinitions(cwd);
|
|
@@ -325,15 +326,20 @@ export async function runInit(
|
|
|
325
326
|
const scriptType = options?.script ?? (process.platform === "win32" ? "ps" : "sh");
|
|
326
327
|
await fs.writeFile(
|
|
327
328
|
path.join(root, "init-options.json"),
|
|
328
|
-
`${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
|
|
329
|
+
`${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", author: options?.author ?? null, preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
|
|
329
330
|
"utf8"
|
|
330
331
|
);
|
|
331
332
|
|
|
332
333
|
await copyDirIfMissing(path.join(projectScaffoldTemplates, "artifacts"), path.join(root, "templates"), copiedAssets);
|
|
333
334
|
for (const artifact of artifactDefs) {
|
|
335
|
+
const markdownTemplatePath = path.join(root, "templates", `${artifact.name}.md`);
|
|
336
|
+
const templateHeadings =
|
|
337
|
+
(await fileExists(markdownTemplatePath))
|
|
338
|
+
? extractRequiredHeadingsFromTemplate(await fs.readFile(markdownTemplatePath, "utf8"))
|
|
339
|
+
: [];
|
|
334
340
|
const schema = {
|
|
335
341
|
...schemaTemplate(artifact.name),
|
|
336
|
-
x_required_headings: artifact.required_headings
|
|
342
|
+
x_required_headings: templateHeadings.length > 0 ? templateHeadings : artifact.required_headings
|
|
337
343
|
};
|
|
338
344
|
await writeFileIfMissing(path.join(root, "schemas", `${artifact.name}.yaml`), yaml.dump(schema));
|
|
339
345
|
await writeFileIfMissing(path.join(root, "prompts", `${artifact.name}.md`), `${promptTemplate(artifact.name, options?.lang ?? "en")}\n`);
|
|
@@ -385,7 +391,8 @@ export async function runInit(
|
|
|
385
391
|
await syncRegistry(cwd);
|
|
386
392
|
const settingsPath = await writeSettings(cwd, {
|
|
387
393
|
lang: (options?.lang ?? "en").trim() || "en",
|
|
388
|
-
ai: options?.ai
|
|
394
|
+
ai: options?.ai,
|
|
395
|
+
author: (options?.author ?? "").trim() || undefined
|
|
389
396
|
});
|
|
390
397
|
return { installedAgentFiles, settingsPath };
|
|
391
398
|
}
|
package/src/normalize.ts
CHANGED
|
@@ -17,6 +17,53 @@ type NormalizeOptions = {
|
|
|
17
17
|
out?: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
function normalizedKey(value: string): string {
|
|
21
|
+
return value
|
|
22
|
+
.normalize("NFD")
|
|
23
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
24
|
+
.replace(/ı/g, "i")
|
|
25
|
+
.replace(/İ/g, "I")
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
28
|
+
.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function extractBriefProductName(rawBrief: string): string | undefined {
|
|
32
|
+
const lines = rawBrief.split(/\r?\n/);
|
|
33
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
34
|
+
const headingMatch = lines[index].match(/^\s*#{1,6}\s+(.+?)\s*$/);
|
|
35
|
+
if (!headingMatch) continue;
|
|
36
|
+
const headingKey = normalizedKey(headingMatch[1]);
|
|
37
|
+
const isProductHeading =
|
|
38
|
+
headingKey === "product name" ||
|
|
39
|
+
headingKey === "project name" ||
|
|
40
|
+
headingKey === "urun adi" ||
|
|
41
|
+
headingKey === "urun ismi";
|
|
42
|
+
if (!isProductHeading) continue;
|
|
43
|
+
|
|
44
|
+
for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
|
|
45
|
+
const rawLine = lines[cursor].trim();
|
|
46
|
+
if (!rawLine) continue;
|
|
47
|
+
if (/^\s*#{1,6}\s+/.test(rawLine)) break;
|
|
48
|
+
const cleaned = rawLine.replace(/^\s*[-*]\s*/, "").trim();
|
|
49
|
+
if (cleaned.length > 0) return cleaned;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function preserveOriginalProductName(
|
|
56
|
+
parsed: Record<string, unknown>,
|
|
57
|
+
rawBrief: string
|
|
58
|
+
): Record<string, unknown> {
|
|
59
|
+
const briefProductName = extractBriefProductName(rawBrief);
|
|
60
|
+
if (!briefProductName) return parsed;
|
|
61
|
+
const generated = typeof parsed.product_name === "string" ? parsed.product_name : "";
|
|
62
|
+
if (!generated.trim()) return { ...parsed, product_name: briefProductName };
|
|
63
|
+
if (normalizedKey(generated) !== normalizedKey(briefProductName)) return parsed;
|
|
64
|
+
return { ...parsed, product_name: briefProductName };
|
|
65
|
+
}
|
|
66
|
+
|
|
20
67
|
function extractJsonObject(raw: string): Record<string, unknown> {
|
|
21
68
|
const trimmed = raw.trim();
|
|
22
69
|
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
@@ -61,17 +108,18 @@ export async function runNormalize(options: NormalizeOptions): Promise<string> {
|
|
|
61
108
|
);
|
|
62
109
|
|
|
63
110
|
const parsed = extractJsonObject(generated.body);
|
|
111
|
+
const preserved = preserveOriginalProductName(parsed, rawBrief);
|
|
64
112
|
const withContracts = {
|
|
65
|
-
...
|
|
113
|
+
...preserved,
|
|
66
114
|
contracts:
|
|
67
|
-
|
|
115
|
+
preserved.contracts ??
|
|
68
116
|
buildContractsFromArrays({
|
|
69
|
-
goals: Array.isArray(
|
|
70
|
-
core_features: Array.isArray(
|
|
71
|
-
?
|
|
117
|
+
goals: Array.isArray(preserved.goals) ? preserved.goals.filter((x): x is string => typeof x === "string") : [],
|
|
118
|
+
core_features: Array.isArray(preserved.core_features)
|
|
119
|
+
? preserved.core_features.filter((x): x is string => typeof x === "string")
|
|
72
120
|
: [],
|
|
73
|
-
constraints: Array.isArray(
|
|
74
|
-
?
|
|
121
|
+
constraints: Array.isArray(preserved.constraints)
|
|
122
|
+
? preserved.constraints.filter((x): x is string => typeof x === "string")
|
|
75
123
|
: []
|
|
76
124
|
})
|
|
77
125
|
};
|
|
@@ -37,7 +37,8 @@ export class OpenAIProvider implements LLMProvider {
|
|
|
37
37
|
const system =
|
|
38
38
|
mode === "normalize"
|
|
39
39
|
? `You normalize messy human product briefs into strict JSON.
|
|
40
|
-
Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields
|
|
40
|
+
Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields.
|
|
41
|
+
Preserve source language and Unicode characters exactly; never transliterate Turkish letters to ASCII.`
|
|
41
42
|
: mode === "semantic_consistency"
|
|
42
43
|
? `You detect semantic inconsistencies between paired artifacts.
|
|
43
44
|
Return valid JSON only: { "issues": [{level, code, check, contract_id, file, message, suggestion}] }.`
|
package/src/settings.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { fileExists } from "./utils";
|
|
|
5
5
|
export type ProdoSettings = {
|
|
6
6
|
lang: string;
|
|
7
7
|
ai?: string;
|
|
8
|
+
author?: string;
|
|
8
9
|
};
|
|
9
10
|
|
|
10
11
|
const DEFAULT_SETTINGS: ProdoSettings = {
|
|
@@ -19,7 +20,8 @@ export async function readSettings(cwd: string): Promise<ProdoSettings> {
|
|
|
19
20
|
const parsed = JSON.parse(raw) as Partial<ProdoSettings>;
|
|
20
21
|
return {
|
|
21
22
|
lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
|
|
22
|
-
ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined
|
|
23
|
+
ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined,
|
|
24
|
+
author: typeof parsed.author === "string" && parsed.author.trim() ? parsed.author.trim() : undefined
|
|
23
25
|
};
|
|
24
26
|
} catch {
|
|
25
27
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -31,4 +33,3 @@ export async function writeSettings(cwd: string, settings: ProdoSettings): Promi
|
|
|
31
33
|
await fs.writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
32
34
|
return path;
|
|
33
35
|
}
|
|
34
|
-
|
package/src/templates.ts
CHANGED
|
@@ -347,6 +347,8 @@ Return JSON object with keys:
|
|
|
347
347
|
Rules:
|
|
348
348
|
- do NOT invent missing critical content
|
|
349
349
|
- keep wording concise and concrete
|
|
350
|
+
- preserve original language and Unicode characters exactly from brief
|
|
351
|
+
- never transliterate Turkish letters (ç, ğ, ı, İ, ö, ş, ü) into ASCII
|
|
350
352
|
- if critical field is missing, return empty and low confidence (<0.7)
|
|
351
353
|
- assign deterministic IDs: goals => G1..Gn, features => F1..Fn, constraints => C1..Cn
|
|
352
354
|
- input files are read-only; never modify, summarize, or rewrite \`brief.md\` in-place
|
package/src/utils.ts
CHANGED
|
@@ -23,6 +23,20 @@ export function timestampSlug(date = new Date()): string {
|
|
|
23
23
|
return date.toISOString().replace(/[:.]/g, "-");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function pad2(value: number): string {
|
|
27
|
+
return String(value).padStart(2, "0");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function artifactFileStamp(date = new Date()): string {
|
|
31
|
+
const year = date.getFullYear();
|
|
32
|
+
const month = pad2(date.getMonth() + 1);
|
|
33
|
+
const day = pad2(date.getDate());
|
|
34
|
+
const hour = pad2(date.getHours());
|
|
35
|
+
const minute = pad2(date.getMinutes());
|
|
36
|
+
const second = pad2(date.getSeconds());
|
|
37
|
+
return `${year}${month}${day}-${hour}${minute}${second}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
export async function listFilesSortedByMtime(dirPath: string): Promise<string[]> {
|
|
27
41
|
const exists = await fileExists(dirPath);
|
|
28
42
|
if (!exists) return [];
|
package/src/validator.ts
CHANGED
|
@@ -59,10 +59,6 @@ export async function validateSchema(
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const sections = sectionTextMap(doc.body);
|
|
62
|
-
const trMode = String((doc.frontmatter as Record<string, unknown>).language ?? "").toLowerCase().startsWith("tr");
|
|
63
|
-
if (trMode) {
|
|
64
|
-
return { issues, requiredHeadings: [] };
|
|
65
|
-
}
|
|
66
62
|
for (const heading of requiredHeadings) {
|
|
67
63
|
if (!doc.body.includes(heading)) {
|
|
68
64
|
issues.push({
|
package/src/workflow-commands.ts
CHANGED
|
@@ -11,7 +11,8 @@ const BASE_WORKFLOW_COMMANDS: WorkflowCommand[] = [
|
|
|
11
11
|
{ name: "prodo-wireframe", cliSubcommand: "wireframe", description: "Generate wireframe artifact." },
|
|
12
12
|
{ name: "prodo-stories", cliSubcommand: "stories", description: "Generate stories artifact." },
|
|
13
13
|
{ name: "prodo-techspec", cliSubcommand: "techspec", description: "Generate technical specification artifact." },
|
|
14
|
-
{ name: "prodo-validate", cliSubcommand: "validate", description: "Run schema and cross-artifact consistency validation." }
|
|
14
|
+
{ name: "prodo-validate", cliSubcommand: "validate", description: "Run schema and cross-artifact consistency validation." },
|
|
15
|
+
{ name: "prodo-fix", cliSubcommand: "fix", description: "Auto-fix artifacts based on validation report and brief." }
|
|
15
16
|
];
|
|
16
17
|
|
|
17
18
|
export const WORKFLOW_COMMANDS: WorkflowCommand[] = BASE_WORKFLOW_COMMANDS;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fix failing artifacts using latest validation report and brief-aligned regeneration.
|
|
3
|
+
agent-role: "Recovery Engineer"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## User Input
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
$ARGUMENTS
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Execution Policy
|
|
13
|
+
|
|
14
|
+
- Execute-first, diagnose-second.
|
|
15
|
+
- Do not run shell commands or CLI commands from inside the agent.
|
|
16
|
+
- Never invoke `prodo-fix`, `prodo fix`, or any `prodo ...` command in shell.
|
|
17
|
+
- Input files are read-only; never modify `brief.md`.
|
|
18
|
+
- Write outputs only under `product-docs/` and `.prodo/`.
|
|
19
|
+
- Do not print full artifact bodies in chat.
|
|
20
|
+
|
|
21
|
+
## Execution
|
|
22
|
+
|
|
23
|
+
1. Minimum prerequisites only:
|
|
24
|
+
- `.prodo/` exists
|
|
25
|
+
- `brief.md` exists
|
|
26
|
+
- `.prodo/briefs/normalized-brief.json` exists (refresh if stale)
|
|
27
|
+
- Validation report exists in `product-docs/reports/`
|
|
28
|
+
|
|
29
|
+
2. Execute fix process immediately:
|
|
30
|
+
- Read latest validation report.
|
|
31
|
+
- Identify failing artifact types and regenerate only impacted chain.
|
|
32
|
+
- Use normalized brief + active templates for regeneration.
|
|
33
|
+
|
|
34
|
+
3. Verify result:
|
|
35
|
+
- Re-run validation.
|
|
36
|
+
- Confirm report status is PASS.
|
|
37
|
+
- Confirm `brief.md` remained unchanged.
|
|
38
|
+
|
|
39
|
+
4. Diagnose only on failure:
|
|
40
|
+
- Inspect internal hooks/scripts only after fix attempt fails.
|
|
41
|
+
- Return concise root cause + next repair action.
|
|
42
|
+
|
|
43
|
+
## Handoff
|
|
44
|
+
|
|
45
|
+
If pass: suggest next command `/prodo-validate`.
|
|
46
|
+
If fail: suggest highest-priority artifact command to regenerate manually.
|