mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 18:33:22 -07:00
feat: add user feedback collection to plan2code-metrics
- Add UserFeedback type (rating 1-10, reason, went well, went poorly) - Collector parses ## User Feedback table from overview.md - Aggregator computes avg_user_rating and feedback_count per cohort - CLI offers interactive feedback collection with pipe-safe escaping - Finalize prompt collects optional feedback as Step 5 (before archival) - Analysis/improvement prompts reference avg_user_rating metric target
This commit is contained in:
@@ -113,6 +113,11 @@ function buildCohort(runs: RunMetrics[], cohortKey: string): CohortMetrics {
|
||||
const avgDocUpdates = avg(runs.map(r => r.step4_finalize.documentation_updates_needed));
|
||||
const archivalRate = rate(runs.map(r => r.step4_finalize.archival_succeeded));
|
||||
|
||||
// User feedback
|
||||
const feedbackRuns = runs.filter(r => r.user_feedback != null);
|
||||
const avgUserRating = avg(feedbackRuns.map(r => r.user_feedback!.overall_rating));
|
||||
const feedbackCount = feedbackRuns.length;
|
||||
|
||||
return {
|
||||
cohort_key: cohortKey,
|
||||
prompt_versions: runs[0].prompt_versions,
|
||||
@@ -146,6 +151,9 @@ function buildCohort(runs: RunMetrics[], cohortKey: string): CohortMetrics {
|
||||
avg_verification_failures_found: avgVerifFailures,
|
||||
avg_documentation_updates_needed: avgDocUpdates,
|
||||
archival_success_rate: archivalRate,
|
||||
|
||||
avg_user_rating: avgUserRating,
|
||||
feedback_count: feedbackCount,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,70 @@ async function flowCollect(): Promise<void> {
|
||||
console.log(` Step 2 (Document): ${metrics.step2_document.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` Step 3 (Implement): ${metrics.step3_implement.present ? (metrics.step3_implement.used_loop_mode ? chalk.green('✓ (loop)') : chalk.yellow('✓ (manual)')) : chalk.gray('—')}`);
|
||||
console.log(` Step 4 (Finalize): ${metrics.step4_finalize.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` User Feedback: ${metrics.user_feedback ? chalk.green(`✓ (${metrics.user_feedback.overall_rating}/10)`) : chalk.gray('—')}`);
|
||||
|
||||
// Offer to collect feedback interactively if not present
|
||||
if (!metrics.user_feedback) {
|
||||
console.log();
|
||||
const wantFeedback = await confirm({
|
||||
message: 'No user feedback found in overview.md. Would you like to provide feedback now?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (wantFeedback) {
|
||||
const rating = await input({
|
||||
message: 'Overall rating (1-10):',
|
||||
validate: (v) => {
|
||||
const n = parseInt(v, 10);
|
||||
return (n >= 1 && n <= 10) ? true : 'Must be a number between 1 and 10';
|
||||
},
|
||||
});
|
||||
const reason = await input({ message: 'Rating reason:' });
|
||||
const wentWell = await input({ message: 'What went well?' });
|
||||
const wentPoorly = await input({ message: 'What went poorly?' });
|
||||
|
||||
// Write the feedback table into overview.md
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
let overviewContent = '';
|
||||
try {
|
||||
overviewContent = fs.readFileSync(overviewPath, 'utf8');
|
||||
} catch { /* empty */ }
|
||||
|
||||
// Escape pipe characters in user text to prevent table parsing issues
|
||||
const esc = (s: string) => s.replace(/\|/g, '\\|');
|
||||
|
||||
const feedbackTable = [
|
||||
'',
|
||||
'## User Feedback',
|
||||
'| Field | Value |',
|
||||
'|-------|-------|',
|
||||
`| Rating | ${rating} |`,
|
||||
`| Reason | ${esc(reason)} |`,
|
||||
`| Went Well | ${esc(wentWell)} |`,
|
||||
`| Went Poorly | ${esc(wentPoorly)} |`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(overviewPath, overviewContent + feedbackTable, 'utf8');
|
||||
console.log(chalk.green('✓ Feedback written to overview.md'));
|
||||
|
||||
// Delete the original run (without feedback) before re-collecting
|
||||
const originalRunPath = path.join(runsDir, `${metrics.run_id}.json`);
|
||||
try { fs.unlinkSync(originalRunPath); } catch { /* ignore */ }
|
||||
|
||||
// Re-collect and re-aggregate
|
||||
const reSpinner = ora('Re-collecting metrics with feedback...').start();
|
||||
const updatedMetrics = await collectRun({
|
||||
specDir,
|
||||
projectName,
|
||||
plan2codeRoot,
|
||||
plan2codeVersion,
|
||||
outputDir: runsDir,
|
||||
});
|
||||
aggregate(runsDir, aggregatedPath);
|
||||
reSpinner.succeed(`Updated: ${updatedMetrics.run_id} (rating: ${updatedMetrics.user_feedback?.overall_rating}/10)`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail(`Collection failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
@@ -292,6 +356,13 @@ async function flowViewStatus(): Promise<void> {
|
||||
console.log(` archival_success_rate: ${metricStatus('archival_success_rate', cohort.archival_success_rate)}`);
|
||||
}
|
||||
|
||||
// User feedback
|
||||
if (cohort.feedback_count > 0) {
|
||||
console.log(chalk.bold(' User Feedback:'));
|
||||
console.log(` avg_user_rating: ${metricStatus('avg_user_rating', cohort.avg_user_rating)}`);
|
||||
console.log(` feedback_count: ${chalk.white(String(cohort.feedback_count))}`);
|
||||
}
|
||||
|
||||
// Compare with previous generation
|
||||
if (i > 0) {
|
||||
const prev = aggregated.cohorts[i - 1];
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
Step2DocumentMetrics,
|
||||
Step3ImplementMetrics,
|
||||
Step4FinalizeMetrics,
|
||||
UserFeedback,
|
||||
} from './types.js';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -399,6 +400,50 @@ export function collectStep4(specDir: string): Step4FinalizeMetrics {
|
||||
};
|
||||
}
|
||||
|
||||
// ── User Feedback ─────────────────────────────────────────────────────────
|
||||
|
||||
export function collectUserFeedback(specDir: string): UserFeedback | null {
|
||||
// Try both active and archived locations for overview.md
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
const specDirName = path.basename(specDir);
|
||||
const parentDir = path.dirname(specDir);
|
||||
const completedPath = path.join(parentDir, '..', 'specs--completed', specDirName, 'overview.md');
|
||||
|
||||
const overview = readFileSafe(overviewPath) ?? readFileSafe(completedPath);
|
||||
if (!overview) return null;
|
||||
|
||||
// Look for ## User Feedback section with a markdown table
|
||||
const feedbackMatch = overview.match(
|
||||
/## User Feedback\s*\n\s*\|[^\n]*\|\s*\n\s*\|[-| ]+\|\s*\n([\s\S]*?)(?=\n##\s|\n*$)/
|
||||
);
|
||||
if (!feedbackMatch) return null;
|
||||
|
||||
const tableBody = feedbackMatch[1];
|
||||
|
||||
// Parse table rows: | Field | Value |
|
||||
// Use regex to split only on unescaped pipes, preserving whitespace in values
|
||||
const rows = tableBody.split('\n').filter(l => l.trim().startsWith('|'));
|
||||
const fields: Record<string, string> = {};
|
||||
for (const row of rows) {
|
||||
// Split on unescaped pipes (not preceded by backslash)
|
||||
const cells = row.split(/(?<!\\)\|/).map(c => c.trim()).filter(c => c.length > 0);
|
||||
if (cells.length >= 2) {
|
||||
const value = cells.slice(1).join('|').replace(/\\\|/g, '|');
|
||||
fields[cells[0].toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const rating = parseInt(fields['rating'] ?? '', 10);
|
||||
if (isNaN(rating) || rating < 1 || rating > 10) return null;
|
||||
|
||||
return {
|
||||
overall_rating: rating,
|
||||
rating_reason: fields['reason'] ?? '',
|
||||
what_went_well: fields['went well'] ?? '',
|
||||
what_went_poorly: fields['went poorly'] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Main collector ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CollectorOptions {
|
||||
@@ -463,6 +508,7 @@ export async function collectRun(opts: CollectorOptions): Promise<RunMetrics> {
|
||||
step2_document: collectStep2(specDir),
|
||||
step3_implement: collectStep3(specDir),
|
||||
step4_finalize: collectStep4(specDir),
|
||||
user_feedback: collectUserFeedback(specDir),
|
||||
};
|
||||
|
||||
// Write to output dir
|
||||
|
||||
@@ -10,6 +10,7 @@ export { runCLI } from './cli.js';
|
||||
export { METRIC_TARGETS } from './types.js';
|
||||
export type {
|
||||
RunMetrics,
|
||||
UserFeedback,
|
||||
PromptVersions,
|
||||
PromptEdit,
|
||||
PromptProposal,
|
||||
|
||||
@@ -38,6 +38,7 @@ The following are the current contents of the plan2code workflow prompt files be
|
||||
| avg_completion_marker_success_rate (Step 3) | ≥ 0.95 | higher is better |
|
||||
| avg_verification_failures_found (Step 4) | ≤ 1.0 | lower is better |
|
||||
| archival_success_rate (Step 4) | ≥ 0.99 | higher is better |
|
||||
| avg_user_rating (Feedback) | ≥ 7.0 | higher is better (1-10 scale, null if no feedback) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ The following diagnosis was produced by the analysis step:
|
||||
- For high `avg_blocker_count`: Add blocker-recovery guidance or prerequisite check instructions.
|
||||
- For low `avg_confidence`: Strengthen the confidence calculation instructions with clearer rubrics.
|
||||
- For low `avg_parallel_groups`: Add explicit guidance for identifying parallel tasks in Step 2.
|
||||
- For low `avg_user_rating`: Review user feedback themes (what_went_well, what_went_poorly) for systemic issues.
|
||||
- Keep each `new_text` as short as possible while still addressing the root cause.
|
||||
|
||||
---
|
||||
|
||||
@@ -62,6 +62,13 @@ export interface Step4FinalizeMetrics {
|
||||
archival_succeeded: boolean | null;
|
||||
}
|
||||
|
||||
export interface UserFeedback {
|
||||
overall_rating: number; // 1-10
|
||||
rating_reason: string;
|
||||
what_went_well: string;
|
||||
what_went_poorly: string;
|
||||
}
|
||||
|
||||
export interface RunMetrics {
|
||||
schema_version: '1.0';
|
||||
run_id: string;
|
||||
@@ -76,6 +83,7 @@ export interface RunMetrics {
|
||||
step2_document: Step2DocumentMetrics;
|
||||
step3_implement: Step3ImplementMetrics;
|
||||
step4_finalize: Step4FinalizeMetrics;
|
||||
user_feedback: UserFeedback | null;
|
||||
}
|
||||
|
||||
export interface PromptEdit {
|
||||
@@ -137,6 +145,10 @@ export interface CohortMetrics {
|
||||
avg_verification_failures_found: number | null;
|
||||
avg_documentation_updates_needed: number | null;
|
||||
archival_success_rate: number | null;
|
||||
|
||||
// User feedback
|
||||
avg_user_rating: number | null;
|
||||
feedback_count: number;
|
||||
}
|
||||
|
||||
export interface AggregatedMetrics {
|
||||
@@ -159,4 +171,5 @@ export const METRIC_TARGETS = {
|
||||
avg_completion_marker_success_rate: { target: 0.95, direction: 'gte' as const },
|
||||
avg_verification_failures_found: { target: 1.0, direction: 'lte' as const },
|
||||
archival_success_rate: { target: 0.99, direction: 'gte' as const },
|
||||
avg_user_rating: { target: 7.0, direction: 'gte' as const },
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user