popilot 0.3.0 → 0.5.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.
Files changed (81) hide show
  1. package/lib/hydrate.mjs +6 -1
  2. package/lib/setup-wizard.mjs +29 -3
  3. package/package.json +1 -1
  4. package/scaffold/.claude/commands/_domain.md.hbs +33 -0
  5. package/scaffold/.claude/commands/analytics.md.hbs +55 -0
  6. package/scaffold/.claude/commands/daily.md.hbs +301 -0
  7. package/scaffold/.claude/commands/dev.md.hbs +62 -0
  8. package/scaffold/.claude/commands/gtm.md +82 -0
  9. package/scaffold/.claude/commands/handoff.md +259 -0
  10. package/scaffold/.claude/commands/market.md +120 -0
  11. package/scaffold/.claude/commands/metrics.md +123 -0
  12. package/scaffold/.claude/commands/oscar-loop.md +436 -0
  13. package/scaffold/.claude/commands/party.md +85 -0
  14. package/scaffold/.claude/commands/plan.md +43 -0
  15. package/scaffold/.claude/commands/poc.md +69 -0
  16. package/scaffold/.claude/commands/research.md +203 -0
  17. package/scaffold/.claude/commands/retro.md +68 -0
  18. package/scaffold/.claude/commands/save.md +440 -0
  19. package/scaffold/.claude/commands/sessions.md +139 -0
  20. package/scaffold/.claude/commands/sprint.md +106 -0
  21. package/scaffold/.claude/commands/start.md +396 -0
  22. package/scaffold/.claude/commands/strategy.md +41 -0
  23. package/scaffold/.claude/commands/task.md +220 -0
  24. package/scaffold/.claude/commands/tracking.md +116 -0
  25. package/scaffold/.claude/commands/validate.md +58 -0
  26. package/scaffold/.context/WORKFLOW.md.hbs +58 -26
  27. package/scaffold/.context/agents/planner.md.hbs +35 -7
  28. package/scaffold/.context/integrations/_registry.yaml +6 -0
  29. package/scaffold/.context/integrations/providers/sqlite_lambda.yaml +24 -0
  30. package/scaffold/.context/integrations/providers/supabase.yaml +34 -0
  31. package/scaffold/.context/integrations/providers/turso_cf.yaml +34 -0
  32. package/scaffold/.context/poc/_skills/build.md +79 -0
  33. package/scaffold/.context/poc/_skills/scope.md +50 -0
  34. package/scaffold/.context/poc/_skills/spec.md +80 -0
  35. package/scaffold/.context/poc/_skills/verify.md +60 -0
  36. package/scaffold/CLAUDE.md.hbs +210 -0
  37. package/scaffold/spec-site/.env.example +11 -0
  38. package/scaffold/spec-site/index.html +2 -2
  39. package/scaffold/spec-site/sql/schema.sql +224 -0
  40. package/scaffold/spec-site/src/api/client.ts +131 -0
  41. package/scaffold/spec-site/src/api/types.ts +177 -0
  42. package/scaffold/spec-site/src/components/Accordion.vue +1 -1
  43. package/scaffold/spec-site/src/components/AppHeader.vue +5 -4
  44. package/scaffold/spec-site/src/components/CoachingCard.vue +1 -1
  45. package/scaffold/spec-site/src/components/ScenarioSwitcher.vue +1 -1
  46. package/scaffold/spec-site/src/composables/navTypes.ts +39 -0
  47. package/scaffold/spec-site/src/composables/pmTypes.ts +134 -0
  48. package/scaffold/spec-site/src/composables/useAuth.ts +139 -0
  49. package/scaffold/spec-site/src/composables/useMemo.ts +51 -40
  50. package/scaffold/spec-site/src/composables/useNavStore.ts +202 -0
  51. package/scaffold/spec-site/src/composables/usePageContent.ts +208 -0
  52. package/scaffold/spec-site/src/composables/usePmStore.ts +224 -0
  53. package/scaffold/spec-site/src/composables/useRetro.ts +181 -95
  54. package/scaffold/spec-site/src/composables/useScenarioStore.ts +74 -30
  55. package/scaffold/spec-site/src/composables/useUser.ts +12 -6
  56. package/scaffold/spec-site/src/data/navigation.ts +7 -42
  57. package/scaffold/spec-site/src/data/types.ts +13 -43
  58. package/scaffold/spec-site/src/main.ts +7 -0
  59. package/scaffold/spec-site/src/pages/PolicyDetail.vue +30 -11
  60. package/scaffold/spec-site/src/pages/PolicyIndex.vue +22 -7
  61. package/scaffold/spec-site/src/pages/retro/RetroActions.vue +3 -3
  62. package/scaffold/spec-site/src/pages/retro/RetroBoard.vue +2 -2
  63. package/scaffold/spec-site/src/pages/retro/RetroHeader.vue +5 -7
  64. package/scaffold/spec-site/src/pages/retro/RetroPage.vue +2 -2
  65. package/scaffold/spec-site/src/pages/shared/NoContentPlaceholder.vue +2 -2
  66. package/scaffold/spec-site/src/pages/shared/PolicyFallback.vue +25 -13
  67. package/scaffold/spec-site/src/router.ts +11 -7
  68. package/scaffold/spec-site/src/styles/base.css +2 -2
  69. package/scaffold/spec-site/src/styles/split-pane.css +1 -1
  70. package/scaffold/spec-site/src/styles/variables.css +7 -7
  71. package/scaffold/spec-site/src/assets/icons/menu/ic_ads.svg +0 -10
  72. package/scaffold/spec-site/src/assets/icons/menu/ic_ads_on.svg +0 -10
  73. package/scaffold/spec-site/src/assets/icons/menu/ic_board.svg +0 -14
  74. package/scaffold/spec-site/src/assets/icons/menu/ic_board_on.svg +0 -14
  75. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard.svg +0 -21
  76. package/scaffold/spec-site/src/assets/icons/menu/ic_dashboard_on.svg +0 -21
  77. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing.svg +0 -20
  78. package/scaffold/spec-site/src/assets/icons/menu/ic_pricing_on.svg +0 -20
  79. package/scaffold/spec-site/src/assets/icons/menu/ic_store.svg +0 -11
  80. package/scaffold/spec-site/src/assets/icons/menu/ic_store_on.svg +0 -11
  81. package/scaffold/spec-site/src/composables/useTurso.ts +0 -160
@@ -0,0 +1,208 @@
1
+ /**
2
+ * usePageContent — Load page content (rules, scenarios, specAreas, versions, meta)
3
+ *
4
+ * Stale-while-revalidate pattern:
5
+ * - localStorage cache for instant render + background API fetch
6
+ * - API failure gracefully falls back to cache
7
+ *
8
+ * In static mode, returns empty content (pages use local data files instead).
9
+ */
10
+
11
+ import { ref, type Ref } from 'vue'
12
+ import { apiGet, isStaticMode } from '@/api/client'
13
+ import type { PageContentApiResponse } from '@/api/types'
14
+ import type { Rule, Scenario, SpecArea, PageVersion, VersionChange } from '@/data/types'
15
+
16
+ // ── Row → Domain mappers ──
17
+
18
+ function rowToRule(row: PageContentApiResponse['rules'][number]): Rule {
19
+ return {
20
+ id: row.id,
21
+ category: row.category,
22
+ name: row.name,
23
+ condition: row.condition,
24
+ severity: row.severity,
25
+ homeMessage: row.home_message,
26
+ action: row.action,
27
+ dataSource: row.data_source,
28
+ implStatus: row.impl_status,
29
+ implNote: row.impl_note ?? undefined,
30
+ actionRoute: row.action_route ?? undefined,
31
+ }
32
+ }
33
+
34
+ // ── Cache helpers ──
35
+
36
+ function cacheKey(pageId: string, sprint: string) {
37
+ return `spec_content_${pageId}_${sprint}`
38
+ }
39
+
40
+ interface CachedContent {
41
+ rules: Record<string, Rule[]>
42
+ scenarios: Scenario<any>[]
43
+ specAreas: SpecArea[]
44
+ version: PageVersion | null
45
+ wireframeMeta: { specTitle: string; routeTitle: string; defaultScenarioId: string } | null
46
+ ts: number
47
+ }
48
+
49
+ function loadCache(pageId: string, sprint: string): CachedContent | null {
50
+ try {
51
+ const raw = localStorage.getItem(cacheKey(pageId, sprint))
52
+ if (!raw) return null
53
+ return JSON.parse(raw) as CachedContent
54
+ } catch {
55
+ return null
56
+ }
57
+ }
58
+
59
+ function saveCache(pageId: string, sprint: string, content: CachedContent) {
60
+ try {
61
+ localStorage.setItem(cacheKey(pageId, sprint), JSON.stringify(content))
62
+ } catch {
63
+ // localStorage full — silently fail
64
+ }
65
+ }
66
+
67
+ // ── Public API ──
68
+
69
+ export interface PageContent {
70
+ rules: Ref<Record<string, Rule[]>>
71
+ scenarios: Ref<Scenario<any>[]>
72
+ specAreas: Ref<SpecArea[]>
73
+ version: Ref<PageVersion | null>
74
+ wireframeMeta: Ref<{ specTitle: string; routeTitle: string; defaultScenarioId: string } | null>
75
+ loading: Ref<boolean>
76
+ error: Ref<string | null>
77
+ load: () => Promise<void>
78
+ getRuleGroup: (group: string) => Rule[]
79
+ }
80
+
81
+ export function usePageContent(pageId: string, sprint: string): PageContent {
82
+ const rules = ref<Record<string, Rule[]>>({})
83
+ const scenarios = ref<Scenario<any>[]>([])
84
+ const specAreas = ref<SpecArea[]>([])
85
+ const version = ref<PageVersion | null>(null)
86
+ const wireframeMeta = ref<{ specTitle: string; routeTitle: string; defaultScenarioId: string } | null>(null)
87
+ const loading = ref(true)
88
+ const error = ref<string | null>(null)
89
+
90
+ function applyContent(content: CachedContent) {
91
+ rules.value = content.rules
92
+ scenarios.value = content.scenarios
93
+ specAreas.value = content.specAreas
94
+ version.value = content.version
95
+ wireframeMeta.value = content.wireframeMeta
96
+ }
97
+
98
+ function transformResponse(data: PageContentApiResponse): CachedContent {
99
+ // Transform rules: group by rule_group
100
+ const rulesByGroup: Record<string, Rule[]> = {}
101
+ for (const row of data.rules) {
102
+ const group = row.rule_group
103
+ if (!rulesByGroup[group]) rulesByGroup[group] = []
104
+ rulesByGroup[group].push(rowToRule(row))
105
+ }
106
+
107
+ // Transform scenarios
108
+ const scenarioList: Scenario<any>[] = data.scenarios.map(row => ({
109
+ id: row.scenario_id,
110
+ label: row.label,
111
+ data: JSON.parse(row.data_json),
112
+ }))
113
+
114
+ // Transform spec areas
115
+ const areaList: SpecArea[] = data.areas.map(row => ({
116
+ id: row.area_id,
117
+ label: row.label,
118
+ shortLabel: row.short_label,
119
+ ruleCount: row.rule_count,
120
+ }))
121
+
122
+ // Transform version
123
+ const vRow = data.versions[0]
124
+ const pageVersion: PageVersion | null = vRow
125
+ ? {
126
+ page: vRow.page_id,
127
+ version: vRow.version,
128
+ lastUpdated: vRow.last_updated,
129
+ sprint: vRow.sprint,
130
+ status: vRow.status as PageVersion['status'],
131
+ changelog: JSON.parse(vRow.changelog) as VersionChange[],
132
+ }
133
+ : null
134
+
135
+ // Transform wireframe meta
136
+ const mRow = data.meta[0]
137
+ const meta = mRow
138
+ ? {
139
+ specTitle: mRow.spec_title,
140
+ routeTitle: mRow.route_title,
141
+ defaultScenarioId: mRow.default_scenario_id,
142
+ }
143
+ : null
144
+
145
+ return {
146
+ rules: rulesByGroup,
147
+ scenarios: scenarioList,
148
+ specAreas: areaList,
149
+ version: pageVersion,
150
+ wireframeMeta: meta,
151
+ ts: Date.now(),
152
+ }
153
+ }
154
+
155
+ async function load() {
156
+ if (isStaticMode()) {
157
+ loading.value = false
158
+ return
159
+ }
160
+
161
+ loading.value = true
162
+ error.value = null
163
+
164
+ // 1. Try cache first (stale-while-revalidate)
165
+ const cached = loadCache(pageId, sprint)
166
+ if (cached) {
167
+ applyContent(cached)
168
+ loading.value = false
169
+ }
170
+
171
+ // 2. Fetch from API (background if cache existed)
172
+ try {
173
+ const { data, error: apiError } = await apiGet<PageContentApiResponse>(
174
+ `/api/v2/page-content/${encodeURIComponent(pageId)}/${encodeURIComponent(sprint)}`,
175
+ )
176
+ if (apiError || !data) throw new Error(apiError ?? 'Unknown error')
177
+
178
+ const fresh = transformResponse(data)
179
+ applyContent(fresh)
180
+ saveCache(pageId, sprint, fresh)
181
+ error.value = null
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : 'Unknown error'
184
+ if (!cached) {
185
+ error.value = msg
186
+ }
187
+ console.warn(`[usePageContent] API fetch failed for ${pageId}/${sprint}: ${msg}`)
188
+ } finally {
189
+ loading.value = false
190
+ }
191
+ }
192
+
193
+ function getRuleGroup(group: string): Rule[] {
194
+ return rules.value[group] ?? []
195
+ }
196
+
197
+ return {
198
+ rules,
199
+ scenarios,
200
+ specAreas,
201
+ version,
202
+ wireframeMeta,
203
+ loading,
204
+ error,
205
+ load,
206
+ getRuleGroup,
207
+ }
208
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * PM Store — API-backed epics, stories & tasks
3
+ *
4
+ * Singleton pattern: refs are module-level, shared across all consumers.
5
+ * In static mode, all data is empty and CRUD operations are disabled.
6
+ */
7
+
8
+ import { ref } from 'vue'
9
+ import { apiGet, apiPost, apiPatch, apiDelete, isStaticMode } from '@/api/client'
10
+ import {
11
+ type PmEpic, type PmStory, type PmTask,
12
+ type StoryStatus, type TaskStatus, type Priority, type EpicStatus,
13
+ mapEpic, mapStory, mapTask,
14
+ } from './pmTypes'
15
+ import type { PmEpicRow, PmStoryRow, PmTaskRow } from '@/api/types'
16
+
17
+ export type { StoryStatus, TaskStatus, Priority, EpicStatus, PmEpic, PmStory, PmTask } from './pmTypes'
18
+ export {
19
+ STORY_STATUSES, TASK_STATUSES, PRIORITIES, AREAS, EPIC_STATUSES,
20
+ STORY_STATUS_LABELS, TASK_STATUS_LABELS, PRIORITY_LABELS, EPIC_STATUS_LABELS,
21
+ } from './pmTypes'
22
+
23
+ // ── Singleton state ──
24
+
25
+ export const pmEpics = ref<PmEpic[]>([])
26
+ export const stories = ref<PmStory[]>([])
27
+ export const tasks = ref<PmTask[]>([])
28
+ export const pmLoaded = ref(false)
29
+
30
+ // ── Load ──
31
+
32
+ export async function loadEpics(): Promise<void> {
33
+ if (isStaticMode()) return
34
+ const { data, error } = await apiGet<{ epics: PmEpicRow[] }>('/api/v2/pm/epics')
35
+ if (!error && data) {
36
+ pmEpics.value = data.epics.map(mapEpic)
37
+ }
38
+ }
39
+
40
+ export async function loadPmData(sprint?: string): Promise<void> {
41
+ if (isStaticMode()) { pmLoaded.value = true; return }
42
+ const params: Record<string, string> = {}
43
+ if (sprint) params.sprint = sprint
44
+
45
+ const { data, error } = await apiGet<{ stories: PmStoryRow[]; tasks: PmTaskRow[] }>(
46
+ '/api/v2/pm/data', params,
47
+ )
48
+
49
+ if (!error && data) {
50
+ stories.value = data.stories.map(mapStory)
51
+ tasks.value = data.tasks.map(mapTask)
52
+ }
53
+
54
+ pmLoaded.value = true
55
+ }
56
+
57
+ // ── Epic CRUD ──
58
+
59
+ export async function addEpic(data: {
60
+ title: string
61
+ description?: string | null
62
+ status?: EpicStatus
63
+ owner?: string | null
64
+ }): Promise<{ id?: number; error?: string }> {
65
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
66
+ const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/epics', {
67
+ title: data.title,
68
+ description: data.description ?? null,
69
+ status: data.status ?? 'active',
70
+ owner: data.owner ?? null,
71
+ })
72
+ if (error) return { error }
73
+ await loadEpics()
74
+ return { id: resp?.id }
75
+ }
76
+
77
+ export async function updateEpic(id: number, data: {
78
+ title?: string
79
+ description?: string | null
80
+ status?: EpicStatus
81
+ owner?: string | null
82
+ }): Promise<{ error?: string }> {
83
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
84
+ const { error } = await apiPatch(`/api/v2/pm/epics/${id}`, data as Record<string, unknown>)
85
+ if (error) return { error }
86
+ await loadEpics()
87
+ return {}
88
+ }
89
+
90
+ export async function deleteEpic(id: number): Promise<{ error?: string }> {
91
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
92
+ const { error } = await apiDelete(`/api/v2/pm/epics/${id}`)
93
+ if (error) return { error }
94
+ await loadEpics()
95
+ return {}
96
+ }
97
+
98
+ // ── Story CRUD ──
99
+
100
+ export async function addStory(data: {
101
+ epicId: number | null
102
+ sprint: string
103
+ title: string
104
+ description?: string | null
105
+ acceptanceCriteria?: string | null
106
+ assignee?: string | null
107
+ status?: StoryStatus
108
+ priority?: Priority
109
+ area?: string
110
+ storyPoints?: number | null
111
+ }): Promise<{ id?: number; error?: string }> {
112
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
113
+ const maxOrder = stories.value
114
+ .filter(s => s.sprint === data.sprint)
115
+ .reduce((max, s) => Math.max(max, s.sortOrder), -1)
116
+
117
+ const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/stories', {
118
+ epicId: data.epicId,
119
+ sprint: data.sprint,
120
+ title: data.title,
121
+ description: data.description ?? null,
122
+ acceptanceCriteria: data.acceptanceCriteria ?? null,
123
+ assignee: data.assignee ?? null,
124
+ status: data.status ?? 'draft',
125
+ priority: data.priority ?? 'medium',
126
+ area: data.area ?? 'FE',
127
+ storyPoints: data.storyPoints ?? null,
128
+ sortOrder: maxOrder + 1,
129
+ })
130
+ if (error) return { error }
131
+ await loadPmData(data.sprint)
132
+ return { id: resp?.id }
133
+ }
134
+
135
+ export async function updateStory(id: number, data: {
136
+ title?: string
137
+ description?: string | null
138
+ acceptanceCriteria?: string | null
139
+ assignee?: string | null
140
+ status?: StoryStatus
141
+ priority?: Priority
142
+ area?: string
143
+ storyPoints?: number | null
144
+ epicId?: number | null
145
+ sprint?: string
146
+ }): Promise<{ error?: string }> {
147
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
148
+ const { error } = await apiPatch(`/api/v2/pm/stories/${id}`, data as Record<string, unknown>)
149
+ if (error) return { error }
150
+ await loadPmData()
151
+ return {}
152
+ }
153
+
154
+ export async function deleteStory(id: number): Promise<{ error?: string }> {
155
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
156
+ const { error } = await apiDelete(`/api/v2/pm/stories/${id}`)
157
+ if (error) return { error }
158
+ await loadPmData()
159
+ return {}
160
+ }
161
+
162
+ // ── Task CRUD ──
163
+
164
+ export async function addTask(data: {
165
+ storyId: number
166
+ title: string
167
+ assignee?: string | null
168
+ description?: string | null
169
+ }): Promise<{ id?: number; error?: string }> {
170
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
171
+ const maxOrder = tasks.value
172
+ .filter(t => t.storyId === data.storyId)
173
+ .reduce((max, t) => Math.max(max, t.sortOrder), -1)
174
+
175
+ const { data: resp, error } = await apiPost<{ id?: number }>('/api/v2/pm/tasks', {
176
+ storyId: data.storyId,
177
+ title: data.title,
178
+ assignee: data.assignee ?? null,
179
+ description: data.description ?? null,
180
+ sortOrder: maxOrder + 1,
181
+ })
182
+ if (error) return { error }
183
+ await loadPmData()
184
+ return { id: resp?.id }
185
+ }
186
+
187
+ export async function updateTask(id: number, data: {
188
+ title?: string
189
+ assignee?: string | null
190
+ status?: TaskStatus
191
+ description?: string | null
192
+ }): Promise<{ error?: string }> {
193
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
194
+ const { error } = await apiPatch(`/api/v2/pm/tasks/${id}`, data as Record<string, unknown>)
195
+ if (error) return { error }
196
+ await loadPmData()
197
+ return {}
198
+ }
199
+
200
+ export async function deleteTask(id: number): Promise<{ error?: string }> {
201
+ if (isStaticMode()) return { error: 'CRUD not available in static mode' }
202
+ const { error } = await apiDelete(`/api/v2/pm/tasks/${id}`)
203
+ if (error) return { error }
204
+ await loadPmData()
205
+ return {}
206
+ }
207
+
208
+ // ── Helpers ──
209
+
210
+ export function getStoriesForEpic(epicId: number): PmStory[] {
211
+ return stories.value.filter(s => s.epicId === epicId)
212
+ }
213
+
214
+ export function getStoriesForSprint(sprint: string): PmStory[] {
215
+ return stories.value.filter(s => s.sprint === sprint)
216
+ }
217
+
218
+ export function getEpicById(id: number): PmEpic | undefined {
219
+ return pmEpics.value.find(e => e.id === id)
220
+ }
221
+
222
+ export function getTasksForStory(storyId: number): PmTask[] {
223
+ return tasks.value.filter(t => t.storyId === storyId)
224
+ }