r2pde-ai 0.1.0
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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/cli.js +97 -0
- package/dist/commands/config-lang.js +32 -0
- package/dist/commands/config-set.js +38 -0
- package/dist/commands/contract-create.js +101 -0
- package/dist/commands/contract-delete.js +45 -0
- package/dist/commands/doctor.js +112 -0
- package/dist/commands/init.js +190 -0
- package/dist/commands/logs.js +35 -0
- package/dist/commands/manifesto-create.js +93 -0
- package/dist/commands/manifesto-delete.js +45 -0
- package/dist/commands/requirement-create.js +100 -0
- package/dist/commands/requirement-delete.js +46 -0
- package/dist/commands/reset.js +33 -0
- package/dist/commands/score-config.js +31 -0
- package/dist/commands/score.js +43 -0
- package/dist/commands/wave-prompt.js +130 -0
- package/dist/core/ai/adapter-factory.js +9 -0
- package/dist/core/ai/ai-adapter.js +1 -0
- package/dist/core/ai/api-adapter.js +38 -0
- package/dist/core/ai/mock-adapter.js +28 -0
- package/dist/core/config.js +61 -0
- package/dist/core/git.js +20 -0
- package/dist/core/logger.js +22 -0
- package/dist/core/paths.js +17 -0
- package/dist/core/scorer.js +135 -0
- package/dist/index.js +1 -0
- package/dist/templates/contract.template.md +14 -0
- package/dist/templates/manifesto.template.md +15 -0
- package/dist/templates/requirement.template.md +17 -0
- package/package.json +50 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getPaths } from '../core/paths.js';
|
|
3
|
+
import { logWarn, logSuccess, logInfo } from '../core/logger.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { confirm } from '@inquirer/prompts';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
export const resetCommand = new Command('reset')
|
|
8
|
+
.description('Clear the prompts folder')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const paths = getPaths(cwd);
|
|
12
|
+
if (!fs.existsSync(paths.root)) {
|
|
13
|
+
logWarn('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
logWarn('This will delete all files in .r2pde-ai/prompts/. This action cannot be undone.');
|
|
17
|
+
logInfo('This will permanently delete all generated prompts. Artifacts (manifestos, contracts, requirements) are NOT affected.');
|
|
18
|
+
const doReset = await confirm({ message: 'Reset prompts folder?', default: false });
|
|
19
|
+
if (!doReset) {
|
|
20
|
+
logInfo('Operation cancelled.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const promptDir = paths.prompts;
|
|
24
|
+
if (fs.existsSync(promptDir)) {
|
|
25
|
+
for (const file of fs.readdirSync(promptDir)) {
|
|
26
|
+
fs.unlinkSync(path.join(promptDir, file));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
30
|
+
fs.ensureFileSync(logPath);
|
|
31
|
+
fs.appendFileSync(logPath, `- ${new Date().toISOString()} | reset | prompts | cleared\n`, { encoding: 'utf8' });
|
|
32
|
+
logSuccess('Prompts folder cleared.');
|
|
33
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { loadConfig, saveConfig } from '../core/config.js';
|
|
3
|
+
import { logInfo, logSuccess, logError } from '../core/logger.js';
|
|
4
|
+
import { input } from '@inquirer/prompts';
|
|
5
|
+
export const scoreConfigCommand = new Command('score:config')
|
|
6
|
+
.description('Configure quality score thresholds')
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const config = loadConfig(cwd);
|
|
10
|
+
logInfo(`Current thresholds:`);
|
|
11
|
+
logInfo(` Green max % issues: ${config.score.green.max}`);
|
|
12
|
+
logInfo(` Yellow max % issues: ${config.score.yellow.max}`);
|
|
13
|
+
logInfo('Maximum percentage of issues allowed for a GREEN score. Below this = healthy project.');
|
|
14
|
+
const green = Number(await input({ message: 'Green threshold max % of issues:', default: String(config.score.green.max), validate: (v) => {
|
|
15
|
+
const n = Number(v);
|
|
16
|
+
return n >= 1 && n < 100 ? true : 'Enter a value between 1 and 99';
|
|
17
|
+
} }));
|
|
18
|
+
logInfo('Maximum percentage of issues allowed for a YELLOW score. Above this = RED.');
|
|
19
|
+
const yellow = Number(await input({ message: 'Yellow threshold max % of issues:', default: String(config.score.yellow.max), validate: (v) => {
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return n > green && n < 100 ? true : 'Yellow must be greater than green and less than 100';
|
|
22
|
+
} }));
|
|
23
|
+
if (green >= yellow) {
|
|
24
|
+
logError('Green threshold must be less than yellow threshold.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
config.score.green.max = green;
|
|
28
|
+
config.score.yellow.max = yellow;
|
|
29
|
+
saveConfig(cwd, config);
|
|
30
|
+
logSuccess(`Updated thresholds: green.max=${green}, yellow.max=${yellow}`);
|
|
31
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { scoreProject } from '../core/scorer.js';
|
|
3
|
+
import { getPaths } from '../core/paths.js';
|
|
4
|
+
import { logError, logWarn, logInfo } from '../core/logger.js';
|
|
5
|
+
import { loadConfig } from '../core/config.js';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
export const scoreCommand = new Command('score')
|
|
9
|
+
.description('Evaluate quality score from current or specified context')
|
|
10
|
+
.option('--from <folder>', 'Evaluate from this folder (manifestos | contracts | requirements)')
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const paths = getPaths(cwd);
|
|
14
|
+
if (!fs.existsSync(paths.root)) {
|
|
15
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const config = loadConfig(cwd);
|
|
19
|
+
const result = scoreProject(cwd, opts.from);
|
|
20
|
+
for (const warn of result.warnings) {
|
|
21
|
+
if (warn.severity === 'error')
|
|
22
|
+
logError(`[${warn.file}] ${warn.issue}`);
|
|
23
|
+
else
|
|
24
|
+
logWarn(`[${warn.file}] ${warn.issue}`);
|
|
25
|
+
}
|
|
26
|
+
logInfo(`\nChecks: ${result.passed}/${result.total} passed, ${result.failed} failed`);
|
|
27
|
+
logInfo(`Score: ${result.percentage}%`);
|
|
28
|
+
let emoji = '🟢', msg = 'Score is GREEN — environment is consistent, proceed.';
|
|
29
|
+
if (result.level === 'yellow') {
|
|
30
|
+
emoji = '🟡';
|
|
31
|
+
msg = 'Score is YELLOW — gaps detected, proceed with awareness.';
|
|
32
|
+
}
|
|
33
|
+
else if (result.level === 'red') {
|
|
34
|
+
emoji = '🔴';
|
|
35
|
+
msg = 'Score is RED — critical issues found, resolve before proceeding.';
|
|
36
|
+
}
|
|
37
|
+
logInfo(`${emoji} ${msg}`);
|
|
38
|
+
// Log
|
|
39
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
40
|
+
const logLine = `- ${new Date().toISOString()} | score | [${result.level}] | [${result.percentage}%] | [${result.passed}/${result.total}] checks passed\n`;
|
|
41
|
+
fs.ensureFileSync(logPath);
|
|
42
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
43
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getPaths } from '../core/paths.js';
|
|
3
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
4
|
+
import { scoreProject } from '../core/scorer.js';
|
|
5
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
6
|
+
import fs from 'fs-extra';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { createAiAdapter } from '../core/ai/adapter-factory.js';
|
|
9
|
+
function logScore(level, msg) {
|
|
10
|
+
let emoji = '🟢';
|
|
11
|
+
if (level === 'yellow')
|
|
12
|
+
emoji = '🟡';
|
|
13
|
+
if (level === 'red')
|
|
14
|
+
emoji = '🔴';
|
|
15
|
+
logInfo(`${emoji} ${msg}`);
|
|
16
|
+
}
|
|
17
|
+
export const wavePromptCommand = new Command('wave:prompt')
|
|
18
|
+
.description('Generate consolidated prompt for current wave')
|
|
19
|
+
.action(async () => {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
const paths = getPaths(cwd);
|
|
22
|
+
if (!fs.existsSync(paths.root)) {
|
|
23
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Step 2 — Score check
|
|
27
|
+
const score = scoreProject(cwd);
|
|
28
|
+
if (score.level === 'red') {
|
|
29
|
+
logScore('red', 'Score is RED. Generating prompt with critical issues may produce poor results.');
|
|
30
|
+
logInfo('Prompt generation with RED score may produce poor results.');
|
|
31
|
+
const doContinue = await confirm({ message: 'Score is RED. Generating prompt with critical issues may produce poor results. Continue?', default: false });
|
|
32
|
+
if (!doContinue) {
|
|
33
|
+
logInfo('Operation cancelled.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
else if (score.level === 'yellow') {
|
|
38
|
+
logScore('yellow', 'Score is YELLOW. Proceeding with warnings.');
|
|
39
|
+
logWarn('Proceeding with warnings.');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
logScore('green', 'Score is healthy.');
|
|
43
|
+
}
|
|
44
|
+
// Step 3 — Ask wave
|
|
45
|
+
logInfo('The implementation wave to generate a prompt for. Each wave covers a specific layer of the project.');
|
|
46
|
+
const wave = await select({
|
|
47
|
+
message: 'Select a wave:',
|
|
48
|
+
choices: [
|
|
49
|
+
{ name: 'Framework', value: 'framework' },
|
|
50
|
+
{ name: 'Architecture', value: 'architecture' },
|
|
51
|
+
{ name: 'Core (errors, logs, security)', value: 'core' },
|
|
52
|
+
{ name: 'Authentication', value: 'authentication' },
|
|
53
|
+
{ name: 'Users', value: 'users' },
|
|
54
|
+
{ name: 'UI/UX', value: 'ui-ux' },
|
|
55
|
+
{ name: 'Tests', value: 'tests' },
|
|
56
|
+
{ name: 'Documentation', value: 'documentation' }
|
|
57
|
+
]
|
|
58
|
+
});
|
|
59
|
+
// Step 4 — Load artifacts
|
|
60
|
+
function readOrPlaceholder(file) {
|
|
61
|
+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '[No artifacts defined]';
|
|
62
|
+
}
|
|
63
|
+
function readAllOrPlaceholder(dir) {
|
|
64
|
+
if (!fs.existsSync(dir))
|
|
65
|
+
return '[No artifacts defined]';
|
|
66
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
67
|
+
if (files.length === 0)
|
|
68
|
+
return '[No artifacts defined]';
|
|
69
|
+
return files.map(f => fs.readFileSync(path.join(dir, f), 'utf8')).join('\n---\n');
|
|
70
|
+
}
|
|
71
|
+
const indexContent = readOrPlaceholder(path.join(paths.root, 'pde.index.md'));
|
|
72
|
+
const manifestosContent = readAllOrPlaceholder(paths.manifestos);
|
|
73
|
+
const contractsContent = readAllOrPlaceholder(paths.contracts);
|
|
74
|
+
const requirementsContent = readAllOrPlaceholder(paths.requirements);
|
|
75
|
+
// Step 5 — Generate prompt
|
|
76
|
+
const now = new Date();
|
|
77
|
+
const tsFmt = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}-${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
|
78
|
+
const filename = `${wave}-${tsFmt}.md`;
|
|
79
|
+
const prompt = `# r2pde-ai — Wave Prompt: ${wave}\nGenerated: ${now.toISOString()}\nScore: ${score.level === 'green' ? '🟢' : score.level === 'yellow' ? '🟡' : '🔴'} ${score.percentage}%\n\n---\n\n## Project Context\n${indexContent}\n\n---\n\n## Manifestos\n${manifestosContent}\n\n---\n\n## Contracts\n${contractsContent}\n\n---\n\n## Requirements\n${requirementsContent}\n\n---\n\n## Wave Objective\nYou are an AI coding assistant working on the ${wave} wave of this project.\nYour task is to implement everything required for this wave following ALL manifestos, contracts, and requirements defined above.\nDo not deviate from the defined architecture, naming conventions, or code patterns.\nGenerate production-ready TypeScript code only.\nAsk no questions — implement based on the context provided.\n\n## Wave: ${wave}\nImplement the ${wave} layer of the project as defined by the artifacts above.\nFollow all contracts strictly. Apply all manifesto principles. Satisfy all requirements.\n`;
|
|
80
|
+
// Step 5.1 — AI Adapter
|
|
81
|
+
const configPath = path.join(paths.root, 'pde.config.json');
|
|
82
|
+
const config = fs.existsSync(configPath) ? fs.readJsonSync(configPath) : {};
|
|
83
|
+
const adapter = createAiAdapter(config);
|
|
84
|
+
if (adapter.isReal()) {
|
|
85
|
+
logInfo('AI API configured — sending prompt to AI...');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
logInfo('No AI API configured — running in offline mode.');
|
|
89
|
+
}
|
|
90
|
+
const ora = (await import('ora')).default;
|
|
91
|
+
const spinner = ora('Generating response...').start();
|
|
92
|
+
let outputPrompt;
|
|
93
|
+
let isApi = adapter.isReal();
|
|
94
|
+
try {
|
|
95
|
+
if (isApi) {
|
|
96
|
+
const metaInstruction = `You are an expert software architect and prompt engineer.\nYour task is to analyze the project context below and generate an optimized, precise prompt for GitHub Copilot.\nThe prompt you generate will be pasted directly into GitHub Copilot to implement the \"${wave}\" wave of this project.\nMake the prompt:\n- Specific and unambiguous\n- Aligned with the defined architecture, patterns, and conventions\n- Structured so Copilot understands exactly what to implement and how\n- Include all relevant constraints from manifestos and contracts\n- Reference acceptance criteria from requirements\nDo not generate code yourself. Generate only the optimized Copilot prompt.\n\n## Project Context\n${prompt}\n\nNow generate the optimized GitHub Copilot prompt for the \"${wave}\" wave:`;
|
|
97
|
+
outputPrompt = await adapter.generate(metaInstruction);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
outputPrompt = await adapter.generate(prompt);
|
|
101
|
+
}
|
|
102
|
+
spinner.succeed('Response received.');
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
spinner.fail('AI API call failed.');
|
|
106
|
+
logError(err instanceof Error ? err.message : 'Unknown error');
|
|
107
|
+
logInfo('Falling back to offline mode — saving prompt for copy/paste.');
|
|
108
|
+
outputPrompt = prompt;
|
|
109
|
+
isApi = false;
|
|
110
|
+
}
|
|
111
|
+
// Step 6 — Save outputPrompt
|
|
112
|
+
const promptsDir = path.join(paths.root, 'prompts');
|
|
113
|
+
fs.ensureDirSync(promptsDir);
|
|
114
|
+
const filePath = path.join(promptsDir, filename);
|
|
115
|
+
fs.writeFileSync(filePath, outputPrompt, { encoding: 'utf8' });
|
|
116
|
+
// Step 7 — Log entry
|
|
117
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
118
|
+
const logLine = `- ${now.toISOString()} | wave:prompt | ${wave} | ${filename} | generated\n`;
|
|
119
|
+
fs.ensureFileSync(logPath);
|
|
120
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
121
|
+
// Step 8 — Output
|
|
122
|
+
if (isApi) {
|
|
123
|
+
logSuccess(`Optimized prompt generated by AI and saved to .r2pde-ai/prompts/${filename}`);
|
|
124
|
+
logInfo('This prompt was optimized by AI. Copy and paste it into GitHub Copilot.');
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
logSuccess(`Prompt generated: .r2pde-ai/prompts/${filename}`);
|
|
128
|
+
logInfo('Copy the content of this file and paste it into GitHub Copilot.');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { MockAdapter } from './mock-adapter.js';
|
|
2
|
+
import { ApiAdapter } from './api-adapter.js';
|
|
3
|
+
export function createAiAdapter(config) {
|
|
4
|
+
const { apiUrl, apiKey } = config.ai;
|
|
5
|
+
if (apiUrl && apiUrl.trim() !== '' && apiKey && apiKey.trim() !== '') {
|
|
6
|
+
return new ApiAdapter({ apiUrl, apiKey, model: config.ai.model });
|
|
7
|
+
}
|
|
8
|
+
return new MockAdapter();
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class ApiAdapter {
|
|
2
|
+
config;
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
isReal() {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
async generate(prompt) {
|
|
10
|
+
const response = await fetch(`${this.config.apiUrl}/v1/messages`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'x-api-key': this.config.apiKey,
|
|
15
|
+
'anthropic-version': '2023-06-01',
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({
|
|
18
|
+
model: this.config.model || 'claude-sonnet-4-20250514',
|
|
19
|
+
max_tokens: 8096,
|
|
20
|
+
messages: [
|
|
21
|
+
{
|
|
22
|
+
role: 'user',
|
|
23
|
+
content: prompt,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
const error = await response.text();
|
|
30
|
+
throw new Error(`AI API error: ${response.status} — ${error}`);
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
return data.content
|
|
34
|
+
.filter(block => block.type === 'text')
|
|
35
|
+
.map(block => block.text)
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export class MockAdapter {
|
|
2
|
+
isReal() {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
async generate(prompt) {
|
|
6
|
+
return [
|
|
7
|
+
'# GitHub Copilot Prompt — Generated by r2pde-ai (Mock Mode)',
|
|
8
|
+
'',
|
|
9
|
+
'> ⚠ This prompt was generated in offline mode (no AI API configured).',
|
|
10
|
+
'> Configure ai.apiUrl and ai.apiKey for AI-optimized prompts.',
|
|
11
|
+
'> To configure: r2pde-ai config:set ai.apiUrl <url>',
|
|
12
|
+
'> To configure: r2pde-ai config:set ai.apiKey <key>',
|
|
13
|
+
'',
|
|
14
|
+
'---',
|
|
15
|
+
'',
|
|
16
|
+
'## Instructions for GitHub Copilot',
|
|
17
|
+
'',
|
|
18
|
+
'Read the full context below and implement the requested wave.',
|
|
19
|
+
'Follow all manifestos, contracts, and requirements strictly.',
|
|
20
|
+
'Generate production-ready TypeScript code only.',
|
|
21
|
+
'Do not ask questions — implement based on the context provided.',
|
|
22
|
+
'',
|
|
23
|
+
'---',
|
|
24
|
+
'',
|
|
25
|
+
prompt,
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export const DEFAULT_CONFIG = {
|
|
4
|
+
version: '0.1.0',
|
|
5
|
+
language: 'en',
|
|
6
|
+
artifactsLanguage: 'en',
|
|
7
|
+
git: {
|
|
8
|
+
autoCommit: false,
|
|
9
|
+
commitMessagePrefix: 'pde:',
|
|
10
|
+
},
|
|
11
|
+
score: {
|
|
12
|
+
green: { max: 30 },
|
|
13
|
+
yellow: { max: 70 },
|
|
14
|
+
red: { max: 100 },
|
|
15
|
+
},
|
|
16
|
+
ai: {
|
|
17
|
+
apiUrl: '',
|
|
18
|
+
apiKey: '',
|
|
19
|
+
model: '',
|
|
20
|
+
},
|
|
21
|
+
project: {
|
|
22
|
+
type: '',
|
|
23
|
+
architecture: '',
|
|
24
|
+
maturity: '',
|
|
25
|
+
hasAuth: false,
|
|
26
|
+
hasPayment: false,
|
|
27
|
+
hasIntegrations: false,
|
|
28
|
+
audience: '',
|
|
29
|
+
stack: '',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export function loadConfig(projectRoot) {
|
|
33
|
+
const configPath = path.resolve(projectRoot, '.r2pde-ai', 'pde.config.json');
|
|
34
|
+
if (!fs.existsSync(configPath)) {
|
|
35
|
+
return DEFAULT_CONFIG;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const fileConfig = fs.readJsonSync(configPath);
|
|
39
|
+
return {
|
|
40
|
+
...DEFAULT_CONFIG,
|
|
41
|
+
...fileConfig,
|
|
42
|
+
git: { ...DEFAULT_CONFIG.git, ...fileConfig.git },
|
|
43
|
+
score: {
|
|
44
|
+
green: { ...DEFAULT_CONFIG.score.green, ...(fileConfig.score?.green || {}) },
|
|
45
|
+
yellow: { ...DEFAULT_CONFIG.score.yellow, ...(fileConfig.score?.yellow || {}) },
|
|
46
|
+
red: { ...DEFAULT_CONFIG.score.red, ...(fileConfig.score?.red || {}) },
|
|
47
|
+
},
|
|
48
|
+
ai: { ...DEFAULT_CONFIG.ai, ...fileConfig.ai },
|
|
49
|
+
project: { ...DEFAULT_CONFIG.project, ...fileConfig.project },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return DEFAULT_CONFIG;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function saveConfig(projectRoot, config) {
|
|
57
|
+
const dir = path.resolve(projectRoot, '.r2pde-ai');
|
|
58
|
+
const configPath = path.resolve(dir, 'pde.config.json');
|
|
59
|
+
fs.ensureDirSync(dir);
|
|
60
|
+
fs.writeJsonSync(configPath, config, { spaces: 2 });
|
|
61
|
+
}
|
package/dist/core/git.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import { logWarn } from './logger.js';
|
|
5
|
+
export function isGitRepo(projectRoot) {
|
|
6
|
+
return fs.existsSync(path.resolve(projectRoot, '.git'));
|
|
7
|
+
}
|
|
8
|
+
export function gitAddAndCommit(projectRoot, message, config) {
|
|
9
|
+
if (!config.git.autoCommit)
|
|
10
|
+
return;
|
|
11
|
+
if (!isGitRepo(projectRoot))
|
|
12
|
+
return;
|
|
13
|
+
try {
|
|
14
|
+
execSync('git add .r2pde-ai/', { cwd: projectRoot, stdio: 'ignore' });
|
|
15
|
+
execSync(`git commit -m "${config.git.commitMessagePrefix} ${message}"`, { cwd: projectRoot, stdio: 'ignore' });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
logWarn('Git commit failed (ignored).');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
export function logSuccess(message) {
|
|
3
|
+
console.log(chalk.green('✔'), message);
|
|
4
|
+
}
|
|
5
|
+
export function logInfo(message) {
|
|
6
|
+
console.log(chalk.blue('ℹ'), message);
|
|
7
|
+
}
|
|
8
|
+
export function logWarn(message) {
|
|
9
|
+
console.log(chalk.yellow('⚠'), message);
|
|
10
|
+
}
|
|
11
|
+
export function logError(message) {
|
|
12
|
+
console.log(chalk.red('✖'), message);
|
|
13
|
+
}
|
|
14
|
+
export function logScore(level, message) {
|
|
15
|
+
const emoji = level === 'green' ? '🟢' : level === 'yellow' ? '🟡' : '🔴';
|
|
16
|
+
let colorFn = chalk.green;
|
|
17
|
+
if (level === 'yellow')
|
|
18
|
+
colorFn = chalk.yellow;
|
|
19
|
+
if (level === 'red')
|
|
20
|
+
colorFn = chalk.red;
|
|
21
|
+
console.log(colorFn(emoji), message);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
export function getPaths(projectRoot) {
|
|
3
|
+
const resolve = (...segments) => path.resolve(projectRoot, ...segments);
|
|
4
|
+
return {
|
|
5
|
+
root: resolve('.r2pde-ai'),
|
|
6
|
+
config: resolve('.r2pde-ai', 'pde.config.json'),
|
|
7
|
+
index: resolve('.r2pde-ai', 'pde.index.md'),
|
|
8
|
+
guide: resolve('.r2pde-ai', 'GUIDE.md'),
|
|
9
|
+
templates: resolve('.r2pde-ai', 'templates'),
|
|
10
|
+
manifestos: resolve('.r2pde-ai', 'manifestos'),
|
|
11
|
+
contracts: resolve('.r2pde-ai', 'contracts'),
|
|
12
|
+
requirements: resolve('.r2pde-ai', 'requirements'),
|
|
13
|
+
waves: resolve('.r2pde-ai', 'waves'),
|
|
14
|
+
prompts: resolve('.r2pde-ai', 'prompts'),
|
|
15
|
+
logs: resolve('.r2pde-ai', 'logs'),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { getPaths } from './paths.js';
|
|
5
|
+
function isKebabCase(filename) {
|
|
6
|
+
return /^[a-z0-9\-]+\.md$/.test(filename);
|
|
7
|
+
}
|
|
8
|
+
function parseMetadata(content) {
|
|
9
|
+
if (!content.startsWith('---'))
|
|
10
|
+
return null;
|
|
11
|
+
const end = content.indexOf('---', 3);
|
|
12
|
+
if (end === -1)
|
|
13
|
+
return null;
|
|
14
|
+
const meta = content.slice(3, end).trim();
|
|
15
|
+
const lines = meta.split(/\r?\n/);
|
|
16
|
+
const obj = {};
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
const [k, ...rest] = line.split(':');
|
|
19
|
+
if (k && rest.length)
|
|
20
|
+
obj[k.trim()] = rest.join(':').trim();
|
|
21
|
+
}
|
|
22
|
+
return obj;
|
|
23
|
+
}
|
|
24
|
+
export function scoreProject(projectRoot, fromPath) {
|
|
25
|
+
const pdePaths = getPaths(projectRoot);
|
|
26
|
+
const paths = {
|
|
27
|
+
manifestos: pdePaths.manifestos,
|
|
28
|
+
contracts: pdePaths.contracts,
|
|
29
|
+
requirements: pdePaths.requirements,
|
|
30
|
+
};
|
|
31
|
+
const folders = ['manifestos', 'contracts', 'requirements'];
|
|
32
|
+
const foldersToProcess = fromPath && folders.includes(fromPath)
|
|
33
|
+
? [fromPath]
|
|
34
|
+
: folders;
|
|
35
|
+
let total = 0, passed = 0, failed = 0;
|
|
36
|
+
const warnings = [];
|
|
37
|
+
const found = { manifestos: false, contracts: false, requirements: false };
|
|
38
|
+
for (const folder of folders) {
|
|
39
|
+
const dir = paths[folder];
|
|
40
|
+
if (fs.existsSync(dir)) {
|
|
41
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
42
|
+
if (files.length > 0)
|
|
43
|
+
found[folder] = true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const folder of foldersToProcess) {
|
|
47
|
+
const dir = paths[folder];
|
|
48
|
+
if (!fs.existsSync(dir))
|
|
49
|
+
continue;
|
|
50
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
const filePath = path.join(dir, file);
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
// Always use POSIX-style for warnings
|
|
55
|
+
const relativePath = `${folder}/${file}`.replace(/\\/g, '/');
|
|
56
|
+
++total;
|
|
57
|
+
if (content.trim().startsWith('---')) {
|
|
58
|
+
++passed;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
++failed;
|
|
62
|
+
warnings.push({ file: relativePath, issue: 'Missing metadata header', severity: 'warning' });
|
|
63
|
+
}
|
|
64
|
+
const meta = parseMetadata(content);
|
|
65
|
+
const endOfMeta = content.indexOf('---', 3);
|
|
66
|
+
++total;
|
|
67
|
+
if (meta && meta.name) {
|
|
68
|
+
++passed;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
++failed;
|
|
72
|
+
warnings.push({ file: relativePath, issue: 'Missing name field in metadata', severity: 'warning' });
|
|
73
|
+
}
|
|
74
|
+
++total;
|
|
75
|
+
if (meta && meta.description) {
|
|
76
|
+
++passed;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
++failed;
|
|
80
|
+
warnings.push({ file: relativePath, issue: 'Missing description field', severity: 'warning' });
|
|
81
|
+
}
|
|
82
|
+
++total;
|
|
83
|
+
if (endOfMeta !== -1 && content.slice(endOfMeta + 3).trim().length > 0) {
|
|
84
|
+
++passed;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
++failed;
|
|
88
|
+
warnings.push({ file: relativePath, issue: 'File has no content beyond metadata', severity: 'warning' });
|
|
89
|
+
}
|
|
90
|
+
++total;
|
|
91
|
+
if (isKebabCase(file)) {
|
|
92
|
+
++passed;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
++failed;
|
|
96
|
+
warnings.push({ file: relativePath, issue: 'Filename is not kebab-case', severity: 'warning' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!fromPath) {
|
|
101
|
+
++total;
|
|
102
|
+
if (found.manifestos) {
|
|
103
|
+
++passed;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
++failed;
|
|
107
|
+
warnings.push({ file: 'manifestos/', issue: 'No manifestos defined', severity: 'error' });
|
|
108
|
+
}
|
|
109
|
+
++total;
|
|
110
|
+
if (found.contracts) {
|
|
111
|
+
++passed;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
++failed;
|
|
115
|
+
warnings.push({ file: 'contracts/', issue: 'No contracts defined', severity: 'error' });
|
|
116
|
+
}
|
|
117
|
+
++total;
|
|
118
|
+
if (found.requirements) {
|
|
119
|
+
++passed;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
++failed;
|
|
123
|
+
warnings.push({ file: 'requirements/', issue: 'No requirements defined', severity: 'error' });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const percentage = total > 0 ? Math.round((passed / total) * 1000) / 10 : 100;
|
|
127
|
+
const issuePercentage = total > 0 ? (failed / total) * 100 : 0;
|
|
128
|
+
const config = loadConfig(projectRoot);
|
|
129
|
+
let level = 'red';
|
|
130
|
+
if (issuePercentage <= config.score.green.max)
|
|
131
|
+
level = 'green';
|
|
132
|
+
else if (issuePercentage <= config.score.yellow.max)
|
|
133
|
+
level = 'yellow';
|
|
134
|
+
return { total, passed, failed, warnings, percentage, level };
|
|
135
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Contract: [Name]
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Describe what this contract enforces.
|
|
5
|
+
|
|
6
|
+
## Rules
|
|
7
|
+
- Rule one (mandatory)
|
|
8
|
+
- Rule two (mandatory)
|
|
9
|
+
|
|
10
|
+
## Violations
|
|
11
|
+
Describe what constitutes a violation of this contract.
|
|
12
|
+
|
|
13
|
+
## Exceptions
|
|
14
|
+
Define known exceptions if any.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Manifesto: [Name]
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
Describe the guiding principle this manifesto establishes.
|
|
5
|
+
|
|
6
|
+
## Principles
|
|
7
|
+
- Principle one
|
|
8
|
+
- Principle two
|
|
9
|
+
- Principle three
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
Define what areas of the project this manifesto applies to.
|
|
13
|
+
|
|
14
|
+
## Exceptions
|
|
15
|
+
Define known exceptions if any.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Requirement: [Name]
|
|
2
|
+
|
|
3
|
+
## Type
|
|
4
|
+
[ ] Functional [ ] Non-functional [ ] Business Rule
|
|
5
|
+
|
|
6
|
+
## Description
|
|
7
|
+
Describe the requirement clearly and objectively.
|
|
8
|
+
|
|
9
|
+
## Acceptance Criteria
|
|
10
|
+
- Criterion one
|
|
11
|
+
- Criterion two
|
|
12
|
+
|
|
13
|
+
## Dependencies
|
|
14
|
+
List any requirements or contracts this depends on.
|
|
15
|
+
|
|
16
|
+
## Notes
|
|
17
|
+
Additional context if needed.
|