From c7aa4763492d8d699960537718026d9c2ce3da5e Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Tue, 24 Feb 2026 16:50:16 +0545 Subject: [PATCH 1/4] feat(spec, analytics): context-aware prompts, refined UX state guards, composite KV keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three generator observation fixes improve spec quality and reduce noise: - Analytics text now conditional on needsWorkerProxy() — static apps suggest lightweight service vs Worker endpoint - Loading/Offline UX states only shown for dynamic apps — removes noise for display-only and minimal-tier specs - Empty state prompts now context-aware (weather→location, stock→ticker, news→topics) instead of generic Plus analytics enhancement: - Worker now generates composite persona:tier KV keys for cross-dimensional dashboard analysis - New "Persona × Tier matrix" panel shows user segmentation insights - No breaking changes — composite keys are additive Tests: +9 (8 generator observation tests + 1 composite key test) = 128 total All tests pass, linting clean, builds successfully. --- src/lib/generator.test.ts | 71 +++++++++++++++++++++++++++ src/lib/generator.ts | 100 ++++++++++++++++++++++++++++++++++---- src/pages/admin.astro | 96 ++++++++++++++++++++++++++++++++++++ worker/src/index.test.ts | 13 +++++ worker/src/index.ts | 13 +++-- 5 files changed, 280 insertions(+), 13 deletions(-) diff --git a/src/lib/generator.test.ts b/src/lib/generator.test.ts index bbda913..a274872 100644 --- a/src/lib/generator.test.ts +++ b/src/lib/generator.test.ts @@ -283,6 +283,77 @@ describe('generateSpec', () => { const spec = generateSpec(minimalAnswers); expect(spec).not.toContain('your AI assistant will handle the technical details'); }); + + // --- Part 1: Generator Observation Fixes --- + + // Issue 1: Analytics "Worker endpoint" text scoping + it('standard tier without Worker: omits Worker endpoint text', () => { + const spec = generateSpec(userContentSavesDataAnswers); + expect(spec).toContain('Analytics'); + expect(spec).not.toContain('Worker endpoint'); + expect(spec).toContain('lightweight analytics service'); + }); + + // Issue 2: Loading/Offline UX States guards + it('display-only user-content: omits Loading and Offline states', () => { + const spec = generateSpec({ + ...minimalAnswers, + dataSource: 'user-content', + userInputType: 'display-only', + }); + expect(spec).not.toContain('**Loading**'); + expect(spec).not.toContain('**Offline**'); + }); + + it('external-data app: includes Loading and Offline states', () => { + const spec = generateSpec(standardAnswers); + expect(spec).toContain('**Loading**'); + expect(spec).toContain('**Offline**'); + }); + + it('static minimal-tier app: omits Loading and Offline', () => { + const spec = generateSpec(minimalAnswers); + expect(spec).not.toContain('**Loading**'); + expect(spec).not.toContain('**Offline**'); + }); + + // Issue 3: Context-aware Empty state prompts + it('weather API: generates location-specific prompt', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Current weather and forecast for a location', + }); + expect(spec).toContain('Prompt user for location'); + expect(spec).toContain('**Empty / First Use**'); + }); + + it('stock tracker API: generates ticker-specific prompt', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Stock prices and market data', + }); + expect(spec).toContain('ticker symbol'); + }); + + it('news feed API: generates topic-specific prompt', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'News articles from various sources', + }); + expect(spec).toContain('feed'); + expect(spec).toContain('topics'); + }); + + it('unknown external-data app: uses generic prompt', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Some unknown data source', + apiKnownName: 'CustomAPI', + }); + expect(spec).toContain('initial input'); + expect(spec).not.toContain('location'); + expect(spec).not.toContain('ticker'); + }); }); describe('generateFilename', () => { diff --git a/src/lib/generator.ts b/src/lib/generator.ts index acdf96e..c11d975 100644 --- a/src/lib/generator.ts +++ b/src/lib/generator.ts @@ -150,6 +150,71 @@ function frameworkForTier(tier: ComplexityTier): string { return 'Astro (static site generator)'; } +/** Generate context-aware empty state prompt text based on API description or known name. + * Falls back to generic text if no API context is available. */ +function generateEmptyStatePrompt(apiDescription?: string, apiKnownName?: string): string { + if (!apiDescription) { + return '- **Empty / First Use** — Prompt user for initial input. Clear call to action.'; + } + + const desc = apiDescription.toLowerCase(); + + // Pattern matching for common API types + if ( + desc.includes('location') || + desc.includes('weather') || + desc.includes('city') || + desc.includes('address') + ) { + return '- **Empty / First Use** — Prompt user for location (city, zip, or coordinates). Clear call to action.'; + } + if ( + desc.includes('stock') || + desc.includes('ticker') || + desc.includes('price') || + desc.includes('market') + ) { + return '- **Empty / First Use** — Prompt user for ticker symbol or company name. Clear call to action.'; + } + if ( + desc.includes('search') || + desc.includes('query') || + desc.includes('keyword') || + desc.includes('term') + ) { + return '- **Empty / First Use** — Prompt user for search query. Clear call to action.'; + } + if ( + desc.includes('news') || + desc.includes('feed') || + desc.includes('article') || + desc.includes('rss') + ) { + return '- **Empty / First Use** — Load initial feed or prompt user to select topics. Clear call to action.'; + } + if (desc.includes('currency') || desc.includes('convert')) { + return '- **Empty / First Use** — Prompt user for amounts and currency pairs. Clear call to action.'; + } + + // Check known API names + if (apiKnownName) { + const knownName = apiKnownName.toLowerCase(); + if ( + knownName.includes('weather') || + knownName.includes('openweather') || + knownName.includes('open-meteo') + ) { + return '- **Empty / First Use** — Prompt user for location. Clear call to action.'; + } + if (knownName.includes('crypto') || knownName.includes('coinbase')) { + return '- **Empty / First Use** — Prompt user for cryptocurrencies to track. Clear call to action.'; + } + } + + // Fallback to generic + return '- **Empty / First Use** — Prompt user for initial input. Clear call to action.'; +} + // --- Section builders --- function sectionTitle(a: Partial): string { @@ -355,9 +420,15 @@ function sectionArchitecture(a: Partial, tier: ComplexityTier): str '- Health endpoint: Add a `/health` route to your Worker returning `ok` (for uptime monitoring).', ); } - lines.push( - '- Analytics: Hosting provider analytics cover page views. For funnel tracking (sign-up → action → conversion), add custom events via `navigator.sendBeacon()` to a Worker endpoint.', - ); + if (needsWorkerProxy(a)) { + lines.push( + '- Analytics: Hosting provider analytics cover page views. For funnel tracking (sign-up → action → conversion), add custom events via `navigator.sendBeacon()` to a Worker endpoint.', + ); + } else { + lines.push( + '- Analytics: Hosting provider analytics cover page views. For funnel tracking (sign-up → action → conversion), consider integrating a lightweight analytics service.', + ); + } lines.push( '- Errors: Log to `console.error()` with context. For production visibility, consider a free error tracker (Sentry free tier: 5K events/month).', ); @@ -408,14 +479,18 @@ function sectionDesign(a: Partial, tier: ComplexityTier): string { function sectionUXStates(a: Partial): string { const lines = ['## UX States', '']; - lines.push( - '- **Loading** — Show loading indicator or skeleton while data loads. Show immediately on interaction.', - ); - - if (hasResolvedExternalData(a)) { + // Loading state: only for apps that fetch or save data + if ( + hasResolvedExternalData(a) || + (hasResolvedUserContent(a) && a.userInputType !== 'display-only') + ) { lines.push( - '- **Empty / First Use** — Prompt user for initial input or location. Clear call to action.', + '- **Loading** — Show loading indicator or skeleton while data loads. Show immediately on interaction.', ); + } + + if (hasResolvedExternalData(a)) { + lines.push(generateEmptyStatePrompt(a.apiDescription, a.apiKnownName)); lines.push( '- **Error (API)** — Friendly error message with "Try again" button. Never a dead end.', ); @@ -448,7 +523,12 @@ function sectionUXStates(a: Partial): string { ); } - lines.push('- **Offline** — Show appropriate message. If PWA with cache, show last-known data.'); + // Offline state: only for apps that fetch external data + if (hasResolvedExternalData(a)) { + lines.push( + '- **Offline** — Show appropriate message. If PWA with cache, show last-known data.', + ); + } lines.push(''); lines.push('### Show/Hide Pattern'); diff --git a/src/pages/admin.astro b/src/pages/admin.astro index 45ea65d..6b5f1a1 100644 --- a/src/pages/admin.astro +++ b/src/pages/admin.astro @@ -68,6 +68,12 @@ import SiteHeader from '../components/SiteHeader.astro';

+ +
+

Persona × Tier matrix

+
+

+
@@ -342,6 +348,81 @@ import SiteHeader from '../components/SiteHeader.astro'; } } + function renderPersonaTierMatrix(data: StatsResponse): void { + const body = document.getElementById('persona-tier-body')!; + const insight = document.getElementById('persona-tier-insight')!; + body.replaceChildren(); + + // Parse composite keys: "spec_generated:persona:tier" + const matrix: Record> = {}; + + for (const day of Object.values(data)) { + for (const [key, count] of Object.entries(day)) { + const parts = key.split(':'); + if (parts[0] === 'spec_generated' && parts.length === 4) { + const [, , persona, tier] = parts; + if (!matrix[persona]) matrix[persona] = {}; + matrix[persona][tier] = (matrix[persona][tier] ?? 0) + count; + } + } + } + + // If no composite data, show empty state + if (Object.keys(matrix).length === 0) { + const empty = document.createElement('p'); + empty.className = 'panel-empty'; + empty.textContent = 'No persona×tier data yet.'; + body.appendChild(empty); + return; + } + + // Render as stacked bars per tier + const tiers = ['minimal', 'standard', 'full']; + const personas = Object.keys(matrix); + + for (const tier of tiers) { + const tierCol = document.createElement('div'); + tierCol.className = 'tier-section'; + + const tierLabel = document.createElement('h3'); + tierLabel.className = 'tier-label'; + tierLabel.textContent = tier.charAt(0).toUpperCase() + tier.slice(1); + tierCol.appendChild(tierLabel); + + let tierTotal = 0; + for (const persona of personas) { + tierTotal += matrix[persona]?.[tier] ?? 0; + } + + for (const persona of personas) { + const count = matrix[persona]?.[tier] ?? 0; + tierCol.appendChild( + createMetricBar( + `${persona} (${pct(count, tierTotal)})`, + count, + Math.max(...personas.map((p) => matrix[p]?.[tier] ?? 0)) || 1, + ), + ); + } + + body.appendChild(tierCol); + } + + // Insight: which persona×tier combo is most common + let topCombo = ''; + let topCount = 0; + for (const [persona, tiers] of Object.entries(matrix)) { + for (const [tier, count] of Object.entries(tiers)) { + if (count > topCount) { + topCount = count; + topCombo = `${persona}-${tier}`; + } + } + } + + insight.textContent = topCombo ? `Most common: ${topCombo} (${topCount})` : ''; + } + function renderDashboard(data: StatsResponse): void { const dates = Object.keys(data); const hasFullData = Object.keys(fullData).length > 0; @@ -368,6 +449,7 @@ import SiteHeader from '../components/SiteHeader.astro'; renderTiers(data); renderSatisfaction(data); renderActions(data); + renderPersonaTierMatrix(data); } // Period selector @@ -674,6 +756,20 @@ import SiteHeader from '../components/SiteHeader.astro'; margin-bottom: var(--space-3); } + /* Persona×Tier section */ + .tier-section { + margin-bottom: var(--space-6); + } + + .tier-label { + font-size: var(--text-xs); + font-weight: var(--weight-medium); + color: var(--color-text-muted); + text-transform: uppercase; + letter-spacing: var(--tracking-wider); + margin-bottom: var(--space-3); + } + /* Satisfaction */ .satisfaction-number { font-family: var(--font-display); diff --git a/worker/src/index.test.ts b/worker/src/index.test.ts index 1a657d3..032d857 100644 --- a/worker/src/index.test.ts +++ b/worker/src/index.test.ts @@ -218,6 +218,19 @@ describe('dimensional keys via round-trip', () => { expect(keys.some((k: string) => k.endsWith(':spec_generated:minimal'))).toBe(true); }); + it('spec_generated: creates composite persona:tier key', async () => { + const kv = createMockKV(); + const env = makeEnv(kv); + await postEvent( + JSON.stringify({ event: 'spec_generated', tier: 'minimal', persona: 'new-builder' }), + env, + ); + + const putCalls = (kv.put as ReturnType).mock.calls; + const keys = putCalls.map((c: string[]) => c[0]); + expect(keys.some((k: string) => k.includes('spec_generated:new-builder:minimal'))).toBe(true); + }); + it('creates dimensional key for question_completed with step index', async () => { const kv = createMockKV(); const env = makeEnv(kv); diff --git a/worker/src/index.ts b/worker/src/index.ts index 935da4c..72e31a3 100644 --- a/worker/src/index.ts +++ b/worker/src/index.ts @@ -78,7 +78,8 @@ function safeDimensionValue(val: unknown): string | null { } /** Derive dimensional KV keys from an event payload. - * Each event = 1–3 writes. Budget: ~300–1000 events/day on free tier (1K writes/day). */ + * Each event = 1–3 writes. Budget: ~300–1000 events/day on free tier (1K writes/day). + * For spec_generated, also creates composite persona:tier key for cross-dimensional analysis. */ function getDimensionalKeys( date: string, event: AllowedEvent, @@ -91,8 +92,14 @@ function getDimensionalKeys( if (v) keys.push(`${date}:persona_selected:${v}`); } if (event === 'spec_generated') { - const v = safeDimensionValue(data.tier); - if (v) keys.push(`${date}:spec_generated:${v}`); + const tier = safeDimensionValue(data.tier); + if (tier) keys.push(`${date}:spec_generated:${tier}`); + + // Add composite persona:tier key for dashboard cross-dimensional analysis + const persona = safeDimensionValue(data.persona); + if (tier && persona) { + keys.push(`${date}:spec_generated:${persona}:${tier}`); + } } if ( event === 'question_completed' && From 4cef72d88f9caf688fe3aa5d1385122b7bd47041 Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Tue, 24 Feb 2026 17:14:51 +0545 Subject: [PATCH 2/4] feat(spec): add localhost-first development sections for progressive implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new sections guide AI assistants to build locally-testable apps: 1. Development Stages — Shows three-stage progression: - Stage 1 (Local): Mock data only, app works immediately on localhost - Stage 2 (Integration): Wire real APIs after UI approved - Stage 3 (Polish): Analytics, caching, advanced features (optional) 2. Mock Data Template — Auto-generated based on API type: - Weather APIs: location, temp, condition, forecast structure - Stock APIs: ticker, price, change fields - News APIs: title, description, publishedAt fields - Generic: Field names, types, sample values for unknown APIs - Guides downstream AI to generate realistic mock data 3. Local Development Checklist — Pre-flight validation: - Verify app builds/runs with mock data - Test all UX states without real API - Get user approval on design before integrating - Validate data structure matches real API All specs now include Development Stages (consistency). External-data and user-saves-data specs get mock template + checklist. Tests: +11 new tests covering all sections and conditions (139 total) Pattern: "Guidepost" approach — static generator signals intent, downstream AI (Claude) implements with full capability. Fixes the "hard localhost dependencies" problem: Generated specs now explicitly encourage mock-first, integrate-later development. --- src/lib/generator.test.ts | 85 ++++++++++++++++++++++ src/lib/generator.ts | 144 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) diff --git a/src/lib/generator.test.ts b/src/lib/generator.test.ts index a274872..858a8b2 100644 --- a/src/lib/generator.test.ts +++ b/src/lib/generator.test.ts @@ -354,6 +354,91 @@ describe('generateSpec', () => { expect(spec).not.toContain('location'); expect(spec).not.toContain('ticker'); }); + + // --- Localhost-first development improvements --- + + it('all specs include Development Stages section', () => { + const minimalSpec = generateSpec(minimalAnswers); + const standardSpec = generateSpec(standardAnswers); + expect(minimalSpec).toContain('## Development Stages'); + expect(standardSpec).toContain('## Development Stages'); + }); + + it('Development Stages shows Stage 1 (Local) for all specs', () => { + const spec = generateSpec(minimalAnswers); + expect(spec).toContain('### Stage 1: Local (Mock Data)'); + expect(spec).toContain('app works immediately on localhost'); + }); + + it('Development Stages shows Stage 2 (Integration) only for specs with data dependencies', () => { + const minimalSpec = generateSpec(minimalAnswers); + const externalDataSpec = generateSpec(standardAnswers); + expect(minimalSpec).not.toContain('### Stage 2: Integration'); + expect(externalDataSpec).toContain('### Stage 2: Integration'); + }); + + it('Development Stages shows Stage 3 (Polish) for specs with data dependencies', () => { + const spec = generateSpec(standardAnswers); + expect(spec).toContain('### Stage 3: Polish'); + }); + + it('weather API includes mock data template with location fields', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Current weather and 7-day forecast for any location', + }); + expect(spec).toContain('### Mock Data (for local development)'); + expect(spec).toContain('location: string'); + expect(spec).toContain('temp: number'); + expect(spec).toContain('condition: string'); + }); + + it('stock API includes mock data template with ticker fields', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Real-time stock prices and market data', + }); + expect(spec).toContain('### Mock Data (for local development)'); + expect(spec).toContain('ticker: string'); + expect(spec).toContain('price: number'); + }); + + it('news API includes mock data template with article fields', () => { + const spec = generateSpec({ + ...standardAnswers, + apiDescription: 'Latest news articles and headlines', + }); + expect(spec).toContain('### Mock Data (for local development)'); + expect(spec).toContain('title: string'); + expect(spec).toContain('description: string'); + expect(spec).toContain('publishedAt: string'); + }); + + it('external-data specs include Local Development Checklist', () => { + const spec = generateSpec(standardAnswers); + expect(spec).toContain('Before integrating external services'); + expect(spec).toContain('App builds and runs locally with mock/fixture data'); + expect(spec).toContain('All UX states testable with mock data'); + }); + + it('specs without external data omit Local Development Checklist', () => { + const spec = generateSpec(minimalAnswers); + expect(spec).not.toContain('Before integrating external services'); + }); + + it('user-saves-data specs include Local Development Checklist', () => { + const spec = generateSpec(userContentSavesDataAnswers); + expect(spec).toContain('Before integrating external services'); + }); + + it('display-only user-content specs omit Local Development Checklist', () => { + const spec = generateSpec({ + ...minimalAnswers, + dataSource: 'user-content', + userInputType: 'display-only', + }); + expect(spec).not.toContain('Before integrating external services'); + }); }); describe('generateFilename', () => { diff --git a/src/lib/generator.ts b/src/lib/generator.ts index c11d975..eb2bf01 100644 --- a/src/lib/generator.ts +++ b/src/lib/generator.ts @@ -42,6 +42,7 @@ export function generateSpec(answers: Partial): string { } sections.push(sectionImplementationOrder(answers, tier)); + sections.push(sectionDevelopmentStages(answers)); sections.push(sectionDeployment(answers, tier)); sections.push(sectionPostDeployment(answers)); sections.push(sectionSuggestedPrompt(answers, tier)); @@ -610,6 +611,12 @@ function sectionWiringGuide(a: Partial, tier: ComplexityTier): stri lines.push('1. Browser fetches data directly from API (no key needed, CORS-friendly)'); lines.push('2. Browser renders data using safe DOM methods (`textContent`, not `innerHTML`)'); } + + // Mock data template for external APIs + if (hasResolvedExternalData(a)) { + lines.push(''); + lines.push(generateMockDataTemplate(a.apiDescription, a.apiKnownName)); + } } // Trust Boundary @@ -653,6 +660,21 @@ function sectionWiringGuide(a: Partial, tier: ComplexityTier): stri lines.push('### Pre-Ship Checklist (for agentic system)'); lines.push('> **IMPORTANT: The repo may be public. Complete before deploying.**'); + // Local Development Checklist (before wiring real APIs) + if ( + hasResolvedExternalData(a) || + (hasResolvedUserContent(a) && a.userInputType !== 'display-only') + ) { + lines.push(''); + lines.push('**Before integrating external services:**'); + lines.push( + '- [ ] App builds and runs locally with mock/fixture data (`npm run dev` works immediately)', + ); + lines.push('- [ ] All UX states testable with mock data: loading, error, empty, success'); + lines.push('- [ ] UI approved by user (show mock data before wiring real API)'); + lines.push('- [ ] Data shape verified (mock matches real API response structure)'); + } + if (needsWorkerProxy(a)) { lines.push(''); lines.push('**Security:**'); @@ -975,6 +997,128 @@ function sectionBudgetMath(a: Partial, tier: ComplexityTier): strin return lines.join('\n'); } +/** Generate mock data template based on API description. + * Provides a shape guide for downstream AI to generate realistic sample values. */ +function generateMockDataTemplate(apiDescription?: string, apiKnownName?: string): string { + if (!apiDescription) return ''; + + const desc = apiDescription.toLowerCase(); + const lines = ['### Mock Data (for local development)', '']; + lines.push( + 'Use this shape while building UI. Downstream AI will generate realistic sample values.', + ); + lines.push(''); + + // Weather APIs + if ( + desc.includes('weather') || + desc.includes('forecast') || + desc.includes('temperature') || + apiKnownName?.toLowerCase().includes('weather') + ) { + lines.push('**Shape:**'); + lines.push('```javascript'); + lines.push('const mockWeather = {'); + lines.push(' location: string, // city name'); + lines.push(' current: {'); + lines.push(' temp: number, // degrees (F or C)'); + lines.push(' condition: string, // weather description'); + lines.push(' icon: string // emoji or icon code'); + lines.push(' },'); + lines.push(' forecast: [ // array of future days'); + lines.push(' { day: string, high: number, low: number, condition: string }'); + lines.push(' ]'); + lines.push('};'); + lines.push('```'); + } + // Stock/market APIs + else if ( + desc.includes('stock') || + desc.includes('price') || + desc.includes('market') || + apiKnownName?.toLowerCase().includes('stock') + ) { + lines.push('**Shape:**'); + lines.push('```javascript'); + lines.push('const mockStocks = ['); + lines.push(' {'); + lines.push(' ticker: string, // symbol (e.g., "AAPL")'); + lines.push(' price: number, // current price'); + lines.push(' change: number, // price change'); + lines.push(' changePercent: number, // percentage change'); + lines.push(' name: string // company name'); + lines.push(' }'); + lines.push('];'); + lines.push('```'); + } + // News/feed APIs + else if (desc.includes('news') || desc.includes('feed') || desc.includes('article')) { + lines.push('**Shape:**'); + lines.push('```javascript'); + lines.push('const mockArticles = ['); + lines.push(' {'); + lines.push(' title: string, // headline'); + lines.push(' description: string, // summary'); + lines.push(' source: string, // news source'); + lines.push(' publishedAt: string, // ISO timestamp'); + lines.push(' url: string // article link'); + lines.push(' }'); + lines.push('];'); + lines.push('```'); + } + // Generic fallback + else { + lines.push('**Shape:**'); + lines.push(''); + lines.push('Define the expected data structure with:'); + lines.push('- Field names (e.g., `id`, `title`, `timestamp`)'); + lines.push('- Data types (string, number, array, object)'); + lines.push('- Sample values or ranges'); + lines.push(''); + lines.push('Downstream AI will generate realistic mock data matching this shape.'); + } + + lines.push(''); + lines.push( + '**Implementation:** Load this mock data while building UI. Replace with real API fetch when ready.', + ); + + return lines.join('\n'); +} + +function sectionDevelopmentStages(a: Partial): string { + const lines = ['## Development Stages', '']; + lines.push( + 'Build locally first, integrate external services later. This lets you test the UI without dependencies.', + ); + lines.push(''); + + lines.push('### Stage 1: Local (Mock Data)'); + lines.push('- Render UI with mock/fixture data (defined in Implementation Order)'); + lines.push('- Test all UX states locally: loading, error, success, empty'); + lines.push('- No external API calls'); + lines.push('- **Run:** `npm run dev` → app works immediately on localhost'); + lines.push(''); + + if (hasResolvedExternalData(a) || hasResolvedUserContent(a)) { + lines.push('### Stage 2: Integration (Real Data)'); + lines.push('- Replace mock data with real API/database fetch'); + lines.push('- Test with actual responses (may differ from mock assumptions)'); + lines.push('- Add retry/error handling for real failure modes'); + lines.push(''); + + lines.push('### Stage 3: Polish (Nice-to-Haves)'); + lines.push('- Analytics tracking'); + if (shouldRecommendPWA(a)) { + lines.push('- Service worker for offline caching'); + } + lines.push('- Performance optimizations'); + lines.push('- Advanced features'); + } + + return lines.join('\n'); +} + function sectionImplementationOrder(a: Partial, tier: ComplexityTier): string { const lines = ['## Implementation Order']; let step = 1; From 9180e2248ddefa34e326dd92d0549ff49b59d816 Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Tue, 24 Feb 2026 17:15:51 +0545 Subject: [PATCH 3/4] chore: bump SW cache to gist-v8 for v1.3.1 release --- public/sw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sw.js b/public/sw.js index 76c3911..b99bb78 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,5 +1,5 @@ // Gist service worker — offline-first static site caching -const CACHE_VERSION = 'gist-v7'; +const CACHE_VERSION = 'gist-v8'; const PRECACHE_URLS = [ '/', '/create/', From 6d218f9614eed43af266db3a19bf07b4adf1e853 Mon Sep 17 00:00:00 2001 From: Vinayak Mishra Date: Tue, 24 Feb 2026 17:16:29 +0545 Subject: [PATCH 4/4] chore: bump version to 1.3.1 and update CHANGELOG --- CHANGELOG.md | 19 +++++++++++++++++++ package.json | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5599b7b..643e7a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.1] - 2026-02-24 + +### Added + +- Generator: Development Stages section (all specs) — progressive three-stage pattern: Local (mock), Integration (real API), Polish (optional) +- Generator: Mock Data templates (external-data specs) — context-aware structure guides (weather: location/temp, stock: ticker/price, news: title/date) +- Generator: Local Development Checklist (data-dependent specs) — pre-flight validation before wiring real APIs +- 11 new tests covering all three new sections and edge cases (139 total) + +### Changed + +- Generator: WiringGuide now includes mock data templates for API-based apps +- Generator: Pre-Ship Checklist now emphasizes building locally first before external integrations +- Service worker: bumped cache version to gist-v8 + +### Why + +Specs now explicitly guide AI assistants to build locally-testable apps (mock-first, integrate-later). Solves "hard localhost dependencies" problem: developers can run `npm run dev` and see the app working immediately, without requiring external API setup upfront. + ## [1.3.0] - 2026-02-24 ### Added diff --git a/package.json b/package.json index 9b4aa2b..76afae3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gistapp", "type": "module", - "version": "1.3.0", + "version": "1.3.1", "private": true, "engines": { "node": ">=22"