feat: ship buildpulse 0.3.0 phases and releases

This commit is contained in:
OpenClaw Bot
2026-05-10 20:04:49 +02:00
parent cc63348344
commit 8218a3417e
8 changed files with 1400 additions and 243 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "buildpulse",
"private": true,
"version": "0.2.1",
"version": "0.3.0",
"type": "module",
"scripts": {
"api": "node --env-file=../.env server/index.mjs",
+880 -197
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -49,6 +49,8 @@ export const createAgentSessionPrompt = (
const grouped = groupFeatures(state.features)
const target = options?.target || 'AI coding agent'
const focusFeature = options?.featureId ? state.features.find((feature) => feature.id === options.featureId) ?? null : grouped.now[0] ?? null
const focusRelease = focusFeature?.release_id ? state.releases.find((release) => release.id === focusFeature.release_id) ?? null : null
const focusPhase = focusFeature?.phase_id ? state.phases.find((phase) => phase.id === focusFeature.phase_id) ?? null : null
const relatedPulses = sortPulsesNewestFirst(state.pulses)
.filter((pulse) => (focusFeature ? pulse.feature_id === focusFeature.id : true))
.slice(0, 6)
@@ -62,6 +64,9 @@ export const createAgentSessionPrompt = (
`- Column: ${columnLabels[feature.column]}`,
`- Priority: ${feature.priority}`,
`- Status: ${feature.status}`,
`- Phase: ${focusPhase?.title || '—'}`,
`- Release: ${focusRelease?.name || '—'}`,
`- Release role: ${feature.release_role || '—'}`,
`- Acceptance criteria: ${feature.acceptance_criteria.length ? feature.acceptance_criteria.join('; ') : 'None yet'}`,
`- Scope notes: ${feature.scope_notes || '—'}`,
].join('\n')
@@ -92,6 +97,11 @@ export const createAgentSessionPrompt = (
'## Next Up',
grouped.next.length ? grouped.next.map((feature) => `- ${feature.title} (${feature.status})`).join('\n') : '- Nothing queued yet.',
'',
'## Current Release Context',
focusRelease
? [`- Release: ${focusRelease.name}`, `- Goal: ${focusRelease.goal}`, `- Status: ${focusRelease.status}`, `- Definition of done: ${focusRelease.definition_of_done.length ? focusRelease.definition_of_done.join('; ') : '—'}`].join('\n')
: '- No release linked to this feature 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')
@@ -122,6 +132,8 @@ export const createJsonExport = (state: AppState) =>
schema_version: state.schema_version,
exported_at: new Date().toISOString(),
project: state.project,
phases: state.phases,
releases: state.releases,
features: state.features,
parking_lot: state.parking_lot,
pulses: state.pulses,
@@ -141,6 +153,29 @@ export const createMarkdownPackage = (state: AppState) => {
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 phasesAndReleases = [
'# Phases and Releases',
'',
...state.phases
.sort((a, b) => a.order - b.order)
.flatMap((phase) => {
const releases = state.releases.filter((release) => release.phase_id === phase.id)
return [
`## ${phase.title}`,
`- Goal: ${phase.goal}`,
`- Status: ${phase.status}`,
`- Notes: ${phase.notes || '—'}`,
...(releases.length
? releases.map(
(release) =>
` - **${release.name}** — ${release.status}\n - Goal: ${release.goal}\n - DoD: ${release.definition_of_done.length ? release.definition_of_done.join('; ') : '—'}\n - Required features: ${release.required_feature_ids.length ? release.required_feature_ids.join(', ') : '—'}\n - Optional features: ${release.optional_feature_ids.length ? release.optional_feature_ids.join(', ') : '—'}\n - Forbidden: ${release.forbidden_feature_titles.length ? release.forbidden_feature_titles.join('; ') : '—'}`,
)
: ['- No releases in this phase yet.']),
'',
]
}),
].join('\n')
const featurePlan = ['# Feature Plan']
;(['now', 'next', 'later', 'done'] as FeatureColumn[]).forEach((column) => {
featurePlan.push(`\n## ${columnLabels[column]}`)
@@ -195,6 +230,11 @@ export const createMarkdownPackage = (state: AppState) => {
'## Current Goal',
state.project.current_goal || '—',
'',
'## Active Phase / Release Structure',
state.releases.length
? state.releases.map((release) => `- ${release.name} (${release.status}) — ${release.goal}`).join('\n')
: '_No releases yet._',
'',
'## Active Features',
grouped.now.length ? grouped.now.map(renderFeature).join('\n\n') : '_None yet._',
'',
@@ -237,6 +277,7 @@ export const createMarkdownPackage = (state: AppState) => {
return {
'PROJECT_SUMMARY.md': projectSummary,
'PHASES_AND_RELEASES.md': phasesAndReleases,
'FEATURE_PLAN.md': featurePlan.join('\n'),
'PARKING_LOT.md': parkingLot,
'PULSE_LOG.md': pulseLog,
+123 -2
View File
@@ -3,18 +3,127 @@ import type { AppState } from '../../store/types'
const seedDate = '2026-05-06T00:00:00+02:00'
export const createSeedState = (): AppState => ({
schema_version: '0.2.0',
schema_version: '0.3.0',
project: {
id: 'project_buildpulse',
name: 'BuildPulse',
one_line_pitch: 'A calm planning cockpit for AI-assisted product building.',
description:
'BuildPulse helps capture features, park distracting ideas, log progress as Pulse events, and export clean context for AI coding agents.',
current_goal: 'Ship v0.1 with Feature Plan, Parking Lot, Pulse Log, and Export.',
current_goal: 'Ship v0.3 with Phases, Releases, and clear release-readiness signals.',
notes: 'First dogfood project: BuildPulse manages BuildPulse.',
created_at: seedDate,
updated_at: seedDate,
},
phases: [
{
id: 'phase_manual_cockpit',
title: 'Phase 1: Manual Cockpit',
goal: 'Prove the small planning cockpit works without turning into fake enterprise sludge.',
status: 'done',
order: 1,
notes: 'Feature Plan, Parking Lot, Pulse Log, and Export shipped first.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'phase_ai_idea_placement',
title: 'Phase 2: AI Idea Placement',
goal: 'Turn raw ideas into guided decisions with explicit accept, park, or reject outcomes.',
status: 'done',
order: 2,
notes: 'AI triage and DECISION pulse logging are proven before release planning expands.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'phase_structured_release_planning',
title: 'Phase 3: Structured Release Planning',
goal: 'Make releases concrete: what is required, what is forbidden, and how close the build is to ready.',
status: 'active',
order: 3,
notes: 'This is the v0.3 step.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'phase_session_handoff',
title: 'Phase 4: Session Handoff',
goal: 'Give agents cleaner, more targeted context packages for implementation sessions.',
status: 'upcoming',
order: 4,
notes: 'Planned v0.4 direction.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'phase_local_cloud_ai_assistant',
title: 'Phase 5: Local/Cloud AI Assistant',
goal: 'Blend local and remote AI assistance without turning the cockpit into router spaghetti.',
status: 'upcoming',
order: 5,
notes: 'Planned v0.5 direction.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'phase_agent_pulse_integration',
title: 'Phase 6: Agent Pulse Integration',
goal: 'Ingest agent activity only after the manual cockpit and release planning are stable.',
status: 'upcoming',
order: 6,
notes: 'This stays out of v0.3 on purpose.',
created_at: seedDate,
updated_at: seedDate,
},
],
releases: [
{
id: 'release_v021_ai_triage',
phase_id: 'phase_ai_idea_placement',
name: 'v0.2.1 — AI Triage Flow Hardening',
goal: 'Make AI triage feel like the default idea-intake path from raw idea to DECISION pulse.',
definition_of_done: [
'AI recommends a placement with risk and smallest safe version.',
'User can accept, park, or reject without losing context.',
'Accepted or rejected triage creates a DECISION pulse.',
'Mobile layout stays readable during the triage flow.',
],
required_feature_ids: ['feature_plan_screen', 'parking_lot_screen', 'pulse_log_screen'],
optional_feature_ids: ['export_screen'],
forbidden_feature_titles: ['Live OpenClaw/Hermes Agent Status', 'WebSocket agent telemetry'],
status: 'shipped',
notes: 'This is the verified gate before v0.3 begins.',
created_at: seedDate,
updated_at: seedDate,
},
{
id: 'release_v030_phases_releases',
phase_id: 'phase_structured_release_planning',
name: 'v0.3 — Phases and Releases',
goal: 'Add release planning structure so the cockpit can answer what phase we are in, what release we are building, and what is forbidden until later.',
definition_of_done: [
'Phases exist as explicit project stages.',
'Releases track goal, definition of done, required features, optional features, and forbidden work.',
'Features can be linked to a phase and a release with required/optional role.',
'A release readiness view shows progress, blockers, recent pulses, and forbidden warnings.',
],
required_feature_ids: ['feature_plan_screen', 'parking_lot_screen', 'pulse_log_screen', 'export_screen'],
optional_feature_ids: [],
forbidden_feature_titles: [
'Live OpenClaw/Hermes Agent Status',
'OpenClaw / Hermes integration',
'WebSocket agent telemetry',
'GitHub / Gitea sync',
'Local/cloud model router',
'Session prompt generator',
],
status: 'in_progress',
notes: 'Keep this on planning structure only. No live integrations yet.',
created_at: seedDate,
updated_at: seedDate,
},
],
features: [
{
id: 'feature_plan_screen',
@@ -29,6 +138,9 @@ export const createSeedState = (): AppState => ({
'User can edit feature details without clutter.',
],
scope_notes: 'This is the home screen. It should answer “what now?” immediately.',
phase_id: 'phase_structured_release_planning',
release_id: 'release_v030_phases_releases',
release_role: 'required',
created_at: seedDate,
updated_at: seedDate,
},
@@ -44,6 +156,9 @@ export const createSeedState = (): AppState => ({
'Risk and future placement are visible.',
],
scope_notes: 'Parking is success behavior, not failure.',
phase_id: 'phase_structured_release_planning',
release_id: 'release_v030_phases_releases',
release_role: 'required',
created_at: seedDate,
updated_at: seedDate,
},
@@ -59,6 +174,9 @@ export const createSeedState = (): AppState => ({
'Pulses can link to features optionally.',
],
scope_notes: 'Manual in v0.1. No live agent ingestion yet.',
phase_id: 'phase_structured_release_planning',
release_id: 'release_v030_phases_releases',
release_role: 'required',
created_at: seedDate,
updated_at: seedDate,
},
@@ -74,6 +192,9 @@ export const createSeedState = (): AppState => ({
'Markdown export includes CLAUDE_CONTEXT.md.',
],
scope_notes: 'Handoff quality matters more than bells and whistles.',
phase_id: 'phase_structured_release_planning',
release_id: 'release_v030_phases_releases',
release_role: 'required',
created_at: seedDate,
updated_at: seedDate,
},
+206 -8
View File
@@ -418,6 +418,23 @@ pre {
gap: 0.65rem;
}
.tap-hint {
margin: 0.45rem 0 0;
color: #8fb3cf;
font-size: 0.9rem;
}
.tap-chip {
align-self: flex-start;
margin-top: 0.15rem;
border-radius: 999px;
padding: 0.22rem 0.65rem;
background: rgba(96, 165, 250, 0.1);
color: #93c5fd;
font-size: 0.75rem;
letter-spacing: 0.02em;
}
.feature-signal-row {
font-size: 0.82rem;
color: #9fb4d9;
@@ -1291,6 +1308,9 @@ body {
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background:
radial-gradient(circle at top right, rgba(45, 212, 191, 0.12), transparent 28%),
linear-gradient(180deg, rgba(13, 19, 36, 0.98), rgba(11, 16, 32, 0.92));
}
.mobile-shell-header h1 {
@@ -1309,6 +1329,9 @@ body {
gap: 0.45rem;
color: #b7c4db;
white-space: nowrap;
padding: 0.35rem 0.55rem;
border-radius: 999px;
background: rgba(255, 255, 255, 0.04);
}
.status-dot,
@@ -1454,6 +1477,29 @@ body {
margin-bottom: 1rem;
}
.triage-status-banner,
.triage-meta-line,
.triage-loading-note {
color: #b7c4db;
}
.triage-status-banner {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
align-items: center;
margin-bottom: 1rem;
padding: 0.75rem 0.9rem;
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.16);
background: rgba(3, 7, 18, 0.42);
}
.triage-meta-line,
.triage-loading-note {
margin: 0;
}
.triage-step-card,
.recommendation-card,
.suggested-item-card,
@@ -1578,9 +1624,49 @@ body {
margin-top: 1rem;
}
.editor-sheet-backdrop {
position: fixed;
inset: 0;
z-index: 75;
display: grid;
place-items: end center;
padding: 1rem;
background: rgba(2, 6, 23, 0.64);
backdrop-filter: blur(10px);
}
.editor-sheet {
width: min(980px, 100%);
max-height: calc(100vh - 2rem);
overflow: auto;
display: grid;
gap: 1rem;
background:
linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(7, 11, 22, 0.98)),
radial-gradient(circle at top, rgba(99, 102, 241, 0.12), transparent 40%);
}
.sheet-summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.sheet-summary-grid.single-column {
grid-template-columns: 1fr;
}
.sheet-actions {
position: sticky;
bottom: 0;
padding-top: 0.75rem;
background: linear-gradient(180deg, rgba(11, 16, 32, 0), rgba(11, 16, 32, 0.94) 22%);
}
@media (max-width: 860px) {
.app-shell {
width: min(100% - 1rem, 100%);
padding-bottom: 5.5rem;
}
.mobile-shell-header,
@@ -1605,7 +1691,8 @@ body {
.triage-recommendation-grid,
.accordion-board,
.focus-grid {
.focus-grid,
.sheet-summary-grid {
grid-template-columns: 1fr;
}
@@ -1614,20 +1701,33 @@ body {
}
.tab-bar {
position: sticky;
position: fixed;
left: 0.5rem;
right: 0.5rem;
bottom: 0.5rem;
top: auto;
z-index: 50;
margin-top: 1rem;
padding: 0.35rem;
background: rgba(8, 12, 24, 0.94);
box-shadow: 0 20px 48px rgba(2, 6, 23, 0.45);
backdrop-filter: blur(16px);
}
.triage-modal-backdrop {
.triage-modal-backdrop,
.editor-sheet-backdrop {
padding: 0.5rem;
}
.triage-modal {
.triage-modal,
.editor-sheet {
max-height: calc(100vh - 1rem);
}
.editor-sheet {
width: 100%;
border-radius: 24px 24px 18px 18px;
}
}
@media (max-width: 520px) {
@@ -1637,6 +1737,42 @@ body {
.card {
padding: 1rem;
border-radius: 20px;
background: rgba(10, 15, 28, 0.92);
box-shadow: 0 10px 28px rgba(2, 6, 23, 0.22);
backdrop-filter: blur(8px);
}
.plan-home-card {
background:
linear-gradient(180deg, rgba(12, 18, 34, 0.98), rgba(10, 15, 28, 0.96)),
radial-gradient(circle at top left, rgba(96, 165, 250, 0.1), transparent 34%);
}
.parking-first-list {
background:
linear-gradient(180deg, rgba(14, 21, 34, 0.98), rgba(10, 15, 28, 0.96)),
radial-gradient(circle at top right, rgba(251, 146, 60, 0.08), transparent 28%);
}
.pulse-timeline-card {
background:
linear-gradient(180deg, rgba(12, 18, 34, 0.98), rgba(10, 15, 28, 0.96)),
radial-gradient(circle at top center, rgba(45, 212, 191, 0.08), transparent 28%);
}
.item-card {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(148, 163, 184, 0.12);
}
.item-card:nth-child(odd) {
background: rgba(255, 255, 255, 0.045);
}
.tap-chip {
background: rgba(255, 255, 255, 0.06);
color: #c7d2fe;
}
.button-row,
@@ -1647,13 +1783,36 @@ body {
.tab-bar {
grid-template-columns: repeat(4, minmax(0, 1fr));
border-radius: 18px;
border-radius: 20px;
}
.tab {
min-height: 2.4rem;
padding: 0.65rem 0.2rem;
font-size: 0.78rem;
min-height: 3rem;
padding: 0.7rem 0.2rem;
font-size: 0.8rem;
}
.editor-sheet-backdrop,
.triage-modal-backdrop {
padding: 0;
place-items: end stretch;
}
.editor-sheet,
.triage-modal {
width: 100%;
max-height: 100vh;
border-radius: 22px 22px 0 0;
padding-bottom: 1.25rem;
}
.sheet-actions {
padding-bottom: 0.25rem;
}
.item-card {
padding: 0.9rem;
border-radius: 16px;
}
}
@@ -1721,3 +1880,42 @@ select {
font-size: 0.78rem;
}
}
.roadmap-hero-card,
.roadmap-phase-card,
.roadmap-release-card {
display: grid;
gap: 1rem;
}
.compact-roadmap-grid {
align-items: start;
}
.roadmap-phase-item,
.roadmap-release-detail {
display: grid;
gap: 0.9rem;
}
.roadmap-readiness-grid,
.roadmap-release-feature-links {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.checklist-stack {
display: grid;
gap: 0.45rem;
}
.checkbox-row {
display: flex;
gap: 0.6rem;
align-items: flex-start;
}
.roadmap-badges {
flex-wrap: wrap;
}
+5 -4
View File
@@ -2,10 +2,11 @@ import type { AiRecommendation, AppState } from './types'
const API_BASE = import.meta.env.VITE_BUILDPULSE_API_BASE || ''
const REQUEST_TIMEOUT_MS = Number(import.meta.env.VITE_BUILDPULSE_API_TIMEOUT_MS || 5000)
const AI_REQUEST_TIMEOUT_MS = Number(import.meta.env.VITE_BUILDPULSE_AI_TIMEOUT_MS || 45000)
const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
const request = async <T>(path: string, init?: RequestInit, timeoutMs = REQUEST_TIMEOUT_MS): Promise<T> => {
const controller = new AbortController()
const timeout = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
const timeout = window.setTimeout(() => controller.abort(), timeoutMs)
let response: Response
try {
@@ -19,7 +20,7 @@ const request = async <T>(path: string, init?: RequestInit): Promise<T> => {
})
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
throw new Error(`BuildPulse API request timed out after ${REQUEST_TIMEOUT_MS}ms`, { cause: error })
throw new Error(`BuildPulse API request to ${path} timed out after ${timeoutMs}ms`, { cause: error })
}
throw error
} finally {
@@ -64,7 +65,7 @@ export const triageIdeaWithAi = async (payload: {
const response = await request<{ ok: boolean; provider: string; recommendation: Omit<AiRecommendation, 'id' | 'created_at' | 'raw_idea' | 'optional_context' | 'context_summary' | 'user_decision' | 'created_feature_id' | 'created_parking_item_id' | 'decision_pulse_id'> }>('/api/ai/triage-idea', {
method: 'POST',
body: JSON.stringify(payload),
})
}, AI_REQUEST_TIMEOUT_MS)
return response.recommendation
}
+68 -1
View File
@@ -3,13 +3,16 @@ import {
AI_DECISIONS,
AI_PLACEMENTS,
FEATURE_COLUMNS,
PHASE_STATUSES,
PULSE_TYPES,
RELEASE_ROLES,
RELEASE_STATUSES,
RISK_LEVELS,
SCHEMA_VERSION,
STORAGE_KEY,
SUPPORTED_SCHEMA_VERSIONS,
} from './types'
import type { AiRecommendation, AppState } from './types'
import type { AiRecommendation, AppState, ProjectPhase, Release } from './types'
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value)
@@ -59,6 +62,47 @@ const normalizeRecommendation = (value: unknown): AiRecommendation | null => {
}
}
const normalizePhase = (value: unknown): ProjectPhase | null => {
if (!isObject(value)) return null
if (!hasRequiredStrings(value, ['id', 'title', 'goal'])) return null
if (typeof value.status !== 'string' || !PHASE_STATUSES.includes(value.status as (typeof PHASE_STATUSES)[number])) return null
return {
id: value.id as string,
title: value.title as string,
goal: value.goal as string,
status: value.status as ProjectPhase['status'],
order: typeof value.order === 'number' ? value.order : 0,
notes: typeof value.notes === 'string' ? value.notes : '',
created_at: typeof value.created_at === 'string' ? value.created_at : new Date().toISOString(),
updated_at: typeof value.updated_at === 'string' ? value.updated_at : new Date().toISOString(),
}
}
const normalizeRelease = (value: unknown): Release | null => {
if (!isObject(value)) return null
if (!hasRequiredStrings(value, ['id', 'phase_id', 'name', 'goal'])) return null
if (typeof value.status !== 'string' || !RELEASE_STATUSES.includes(value.status as (typeof RELEASE_STATUSES)[number])) return null
const toStrings = (items: unknown) =>
Array.isArray(items) ? items.filter((item): item is string => typeof item === 'string' && Boolean(item.trim())) : []
return {
id: value.id as string,
phase_id: value.phase_id as string,
name: value.name as string,
goal: value.goal as string,
definition_of_done: toStrings(value.definition_of_done),
required_feature_ids: toStrings(value.required_feature_ids),
optional_feature_ids: toStrings(value.optional_feature_ids),
forbidden_feature_titles: toStrings(value.forbidden_feature_titles),
status: value.status as Release['status'],
notes: typeof value.notes === 'string' ? value.notes : '',
created_at: typeof value.created_at === 'string' ? value.created_at : new Date().toISOString(),
updated_at: typeof value.updated_at === 'string' ? value.updated_at : new Date().toISOString(),
}
}
export const normalizeAppState = (value: unknown): AppState | null => {
if (!isObject(value)) return null
if (!SUPPORTED_SCHEMA_VERSIONS.includes(value.schema_version as (typeof SUPPORTED_SCHEMA_VERSIONS)[number])) return null
@@ -100,10 +144,33 @@ export const normalizeAppState = (value: unknown): AppState | null => {
const recommendations = Array.isArray(value.ai_recommendations)
? value.ai_recommendations.map(normalizeRecommendation).filter((entry): entry is AiRecommendation => Boolean(entry))
: []
const seedState = createSeedState()
const phases = Array.isArray(value.phases)
? value.phases.map(normalizePhase).filter((entry): entry is ProjectPhase => Boolean(entry))
: seedState.phases
const releases = Array.isArray(value.releases)
? value.releases.map(normalizeRelease).filter((entry): entry is Release => Boolean(entry))
: seedState.releases
const normalizedFeatures = value.features.map((feature) => {
if (!isObject(feature)) return feature
const releaseRole = typeof feature.release_role === 'string' && RELEASE_ROLES.includes(feature.release_role as (typeof RELEASE_ROLES)[number])
? feature.release_role
: undefined
return {
...feature,
phase_id: typeof feature.phase_id === 'string' ? feature.phase_id : undefined,
release_id: typeof feature.release_id === 'string' ? feature.release_id : undefined,
release_role: releaseRole,
}
})
return {
...(value as unknown as AppState),
schema_version: SCHEMA_VERSION,
phases,
releases,
features: normalizedFeatures as AppState['features'],
ai_recommendations: recommendations,
}
}
+49 -3
View File
@@ -1,6 +1,6 @@
export const STORAGE_KEY = 'buildpulse.v1'
export const SCHEMA_VERSION = '0.2.0'
export const SUPPORTED_SCHEMA_VERSIONS = ['0.1.0', '0.2.0'] as const
export const SCHEMA_VERSION = '0.3.0'
export const SUPPORTED_SCHEMA_VERSIONS = ['0.1.0', '0.2.0', '0.2.1', '0.3.0'] as const
export const FEATURE_COLUMNS = ['now', 'next', 'later', 'done'] as const
export type FeatureColumn = (typeof FEATURE_COLUMNS)[number]
@@ -23,6 +23,15 @@ export type FeatureStatus = (typeof FEATURE_STATUSES)[number]
export const RISK_LEVELS = ['low', 'medium', 'high', 'dangerous'] as const
export type RiskLevel = (typeof RISK_LEVELS)[number]
export const PHASE_STATUSES = ['upcoming', 'active', 'done'] as const
export type PhaseStatus = (typeof PHASE_STATUSES)[number]
export const RELEASE_STATUSES = ['not_ready', 'in_progress', 'testing', 'ready_to_ship', 'shipped'] as const
export type ReleaseStatus = (typeof RELEASE_STATUSES)[number]
export const RELEASE_ROLES = ['required', 'optional'] as const
export type ReleaseRole = (typeof RELEASE_ROLES)[number]
export const PULSE_TYPES = [
'INTENT',
'ACTION',
@@ -54,6 +63,32 @@ export interface Project {
updated_at: string
}
export interface ProjectPhase {
id: string
title: string
goal: string
status: PhaseStatus
order: number
notes: string
created_at: string
updated_at: string
}
export interface Release {
id: string
phase_id: string
name: string
goal: string
definition_of_done: string[]
required_feature_ids: string[]
optional_feature_ids: string[]
forbidden_feature_titles: string[]
status: ReleaseStatus
notes: string
created_at: string
updated_at: string
}
export interface Feature {
id: string
title: string
@@ -63,6 +98,12 @@ export interface Feature {
status: FeatureStatus
acceptance_criteria: string[]
scope_notes: string
phase_id?: string
release_id?: string
release_role?: ReleaseRole
triaged_at?: string
triage_trace_id?: string
triage_confidence_score?: number
created_at: string
updated_at: string
}
@@ -74,6 +115,9 @@ export interface ParkingLotItem {
reason_parked: string
possible_future_placement: string
risk_level: RiskLevel
triaged_at?: string
triage_trace_id?: string
triage_confidence_score?: number
created_at: string
updated_at: string
}
@@ -130,6 +174,8 @@ export interface Settings {
export interface AppState {
schema_version: string
project: Project
phases: ProjectPhase[]
releases: Release[]
features: Feature[]
parking_lot: ParkingLotItem[]
pulses: PulseEvent[]
@@ -137,4 +183,4 @@ export interface AppState {
settings: Settings
}
export type TabKey = 'status' | 'feature-plan' | 'parking-lot' | 'pulse-log' | 'export'
export type TabKey = 'status' | 'feature-plan' | 'roadmap' | 'parking-lot' | 'pulse-log' | 'export'