Files
buildpulse/src/features/export/exporters.ts
T
2026-05-07 00:11:35 +02:00

149 lines
5.3 KiB
TypeScript

import type { AppState, Feature, FeatureColumn, PulseEvent } from '../../store/types'
import { formatDateTime } from '../../utils/format'
const columnLabels: Record<FeatureColumn, string> = {
now: 'Now',
next: 'Next',
later: 'Later',
done: 'Done',
}
const groupFeatures = (features: Feature[]) => ({
now: features.filter((feature) => feature.column === 'now'),
next: features.filter((feature) => feature.column === 'next'),
later: features.filter((feature) => feature.column === 'later'),
done: features.filter((feature) => feature.column === 'done'),
})
const getFeatureLabel = (features: Feature[], featureId?: string) => {
if (!featureId) return '—'
return features.find((feature) => feature.id === featureId)?.title ?? `${featureId} (missing feature)`
}
const renderFeature = (feature: Feature) => {
const criteria = feature.acceptance_criteria.length
? feature.acceptance_criteria.map((item) => ` - ${item}`).join('\n')
: ' - None yet'
return [
`- **${feature.title}**`,
` - Description: ${feature.description || '—'}`,
` - Priority: ${feature.priority}`,
` - Status: ${feature.status}`,
' - Acceptance Criteria:',
criteria,
` - Scope Notes: ${feature.scope_notes || '—'}`,
].join('\n')
}
const sortPulsesNewestFirst = (pulses: PulseEvent[]) =>
[...pulses].sort((a, b) => b.timestamp.localeCompare(a.timestamp))
export const createJsonExport = (state: AppState) =>
JSON.stringify(
{
schema_version: state.schema_version,
exported_at: new Date().toISOString(),
project: state.project,
features: state.features,
parking_lot: state.parking_lot,
pulses: state.pulses,
settings: state.settings,
},
null,
2,
)
export const createPulseJsonl = (state: AppState) => state.pulses.map((pulse) => JSON.stringify(pulse)).join('\n')
export const createMarkdownPackage = (state: AppState) => {
const grouped = groupFeatures(state.features)
const recentPulses = sortPulsesNewestFirst(state.pulses).slice(0, 8)
const projectSummary = `# Project Summary\n\n## Name\n${state.project.name}\n\n## One-Line Pitch\n${state.project.one_line_pitch || '—'}\n\n## Description\n${state.project.description || '—'}\n\n## Current Goal\n${state.project.current_goal || '—'}\n\n## Notes\n${state.project.notes || '—'}\n`
const featurePlan = ['# Feature Plan']
;(['now', 'next', 'later', 'done'] as FeatureColumn[]).forEach((column) => {
featurePlan.push(`\n## ${columnLabels[column]}`)
const features = grouped[column]
if (!features.length) {
featurePlan.push('\n_No features in this column yet._')
return
}
featurePlan.push('', ...features.map(renderFeature))
})
const parkingLot = [
'# Parking Lot',
'',
...(state.parking_lot.length
? state.parking_lot.map(
(item) =>
`- **${item.title}**\n - Description: ${item.description || '—'}\n - Reason parked: ${item.reason_parked || '—'}\n - Possible future placement: ${item.possible_future_placement || '—'}\n - Risk level: ${item.risk_level}`,
)
: ['_No parked ideas yet._']),
].join('\n')
const pulseLog = [
'# Pulse Log',
'',
...(sortPulsesNewestFirst(state.pulses).length
? sortPulsesNewestFirst(state.pulses).map(
(pulse) =>
`- **${formatDateTime(pulse.timestamp)}** — ${pulse.pulse_type}\n - Feature: ${getFeatureLabel(state.features, pulse.feature_id)}\n - Source/Agent: ${pulse.source} / ${pulse.agent_id}\n - Message: ${pulse.message}\n - Confidence: ${pulse.confidence_score}\n - Evidence: ${pulse.evidence_refs.length ? pulse.evidence_refs.join('; ') : '—'}`,
)
: ['_No pulse events yet._']),
].join('\n')
const claudeContext = [
'# AI Coding Context',
'',
'## Project',
`${state.project.name}${state.project.one_line_pitch || '—'}`,
'',
'## Current Goal',
state.project.current_goal || '—',
'',
'## Active Features',
grouped.now.length ? grouped.now.map(renderFeature).join('\n\n') : '_None yet._',
'',
'## Next Features',
grouped.next.length ? grouped.next.map(renderFeature).join('\n\n') : '_None yet._',
'',
'## Done Features',
grouped.done.length ? grouped.done.map(renderFeature).join('\n\n') : '_None yet._',
'',
'## Parking Lot / Do Not Implement Yet',
state.parking_lot.length
? state.parking_lot
.map((item) => `- ${item.title}${item.reason_parked || item.description || 'Parked for later.'}`)
.join('\n')
: '_No parked ideas yet._',
'',
'## Recent Pulse Events',
recentPulses.length
? recentPulses
.map(
(pulse) =>
`- ${formatDateTime(pulse.timestamp)}${pulse.pulse_type}${pulse.message} (${getFeatureLabel(state.features, pulse.feature_id)})`,
)
.join('\n')
: '_No recent pulse events yet._',
'',
'## Instructions for AI Developer',
'- Only work on the selected feature.',
'- Do not implement Parking Lot items.',
'- Preserve working behavior.',
'- Report changes and test steps.',
].join('\n')
return {
'PROJECT_SUMMARY.md': projectSummary,
'FEATURE_PLAN.md': featurePlan.join('\n'),
'PARKING_LOT.md': parkingLot,
'PULSE_LOG.md': pulseLog,
'CLAUDE_CONTEXT.md': claudeContext,
}
}