Phase 4.1: AI Prompt Enhancement — Spare Parts Deep Identification Artifacts: - 4.1-RESEARCH.md: Web scraping patterns, spare-parts classification, integration architecture - 4.1-PLAN-01.md (Wave 1): Spare-parts whitelist + AI prompt enhancement (4 tasks) - 4.1-PLAN-02.md (Wave 2): Web scraping service + backend integration (6 tasks) - 4.1-PLAN-03.md (Wave 3): Frontend integration + end-to-end testing (7 tasks) All 17 tasks verified: ✓ Concrete action steps with exact function signatures and file paths ✓ 100% verifiable acceptance criteria (grep, pytest, vitest, imports) ✓ Architecture aligned with all 11 CONTEXT.md decisions ✓ CLAUDE.md compliance: TypeScript strict, API tests, UI fidelity ✓ Wave dependencies correctly ordered ✓ Risk mitigation: rate limiting, timeout handling, offline graceful degradation Ready for execution via /gsd-execute-phase 4.1
27 KiB
Phase 4.1 Research: AI Prompt Enhancement — Spare Parts Deep Identification
Research Date: 2026-04-22
Scope: Web scraping implementation, spare-parts classification, AI prompt enhancement, search result parsing, backend/frontend integration, and performance/scalability.
1. Web Scraping Best Practices: Python Requests + BeautifulSoup
Key Findings
Approach & Risks:
- Direct Google scraping is technically feasible but risky: Google actively detects and blocks scrapers with 429 (Too Many Requests) errors, CAPTCHA challenges, and IP bans.
- Terms of Service violation: Google's ToS explicitly forbids scraping search results.
- HTML structure volatility: Google changes CSS selectors and HTML markup frequently, breaking scrapers.
- Practical reality: Direct scraping works for low-volume scenarios (tens of requests/hour) with proper mitigations.
Safer Alternatives:
- SerpAPI / Similar APIs: Officially maintained, handles blocking/rotation, but costs money ($5-50/month depending on volume).
- Bing scraping: Less aggressively blocked than Google, similar HTML structure, viable fallback.
- Manufacturer sites (Dell, HP, Kingston, Crucial): Most reliable source for spare-part specs.
- GitHub Issues / StackOverflow: Often contain real-world component usage and specifications.
Recommended Hybrid Approach:
- Primary: Search manufacturer specs directly (most accurate).
- Fallback 1: Bing web search with BeautifulSoup.
- Fallback 2: Google search (if Bing returns no results).
- Fallback 3: Return AI-extracted data only (graceful offline degradation).
Rate Limiting Strategies
Implementation:
- Delay between requests: 2-5 seconds minimum (random jitter recommended).
- User-Agent rotation: Cycle through 10+ realistic User-Agent strings (Chrome, Firefox, Safari across Windows/Mac/Linux).
- Exponential backoff: 1s → 2s → 4s → 8s → fail.
- Token bucket algorithm: Max 0.2 requests/second (1 request per 5 seconds) per IP.
User-Agent Pool (Examples):
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (Chrome/120.0.0.0)
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (Firefox/121.0)
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (Safari/537.36)
Error Handling & Timeout Strategies
HTTP Status Codes:
- 429 (Too Many Requests): Wait 10 seconds, retry once, then fail gracefully.
- 403 (Forbidden): IP blocked; rotate User-Agent, increase delay, or skip.
- 500+ (Server Error): Retry with exponential backoff.
- Timeout (>10s): Abort search, return AI data only, log warning.
CAPTCHA Detection:
- BeautifulSoup can detect CAPTCHA forms by checking for
<form>withrecaptchakeywords. - If detected: Abort search immediately, return AI data, log incident.
Latency Profile:
- Typical Google request: 2-8 seconds.
- BeautifulSoup HTML parsing: 100-500ms.
- Regex spec extraction: 10-50ms.
- Total end-to-end: 3-15 seconds (up to 30s with retries).
2. Spare-Parts Classification Strategy
Comprehensive Whitelist
Spare-Part Categories (Include These):
- Memory: RAM, DRAM, DDR3, DDR4, DDR5, SODIMM, DIMM
- Storage: SSD, NVME, M.2, SATA, HDD, hard drive, solid state drive
- Processors: CPU, processor, APU, GPU, graphics card, discrete GPU
- Power: PSU (power supply unit), adapter, power module (NOT cables/cords)
- Expansion Cards: PCIe, PCI, RAID controller, network card (NIC), graphics card
- Cooling: Heatsink, CPU cooler, thermal solution
- Motherboards: Motherboard, BIOS, chipset
Consumables to Exclude:
- Cables: SATA cables, USB cables, Ethernet cables, power cords.
- Fasteners: Screws, washers, bolts, standoffs.
- Adhesives/Thermal Materials: Thermal paste, thermal pads, adhesive tapes.
- Connectors: Plugs, sockets, adapters (unless branded components).
Edge Case: Power Supplies
- Spare part: "Corsair RM850x 850W Power Supply Unit" (replaceable, has specs).
- Consumable: "6ft Power Cable" or "AC Power Cord" (generic utility item).
Fuzzy Matching Implementation
Strategy:
- Exact keyword match (highest priority): Check if extracted Category contains exact whitelist terms (RAM, SSD, CPU, GPU, PSU).
- Fuzzy matching (Levenshtein distance, 70-80% threshold):
- "Random Access Memory" → matches "RAM"
- "Solid State Disk" → matches "SSD"
- Regex patterns (fallback):
\bRAM\b|\bDRAM\b|\bDDR\d\b→ Memory component.\bSSD\b|\bNVME\b|\bM\.2\b→ Storage component.
- Exclusion patterns (reject consumables):
^(cable|cord|fastener|screw|adhesive|thermal paste)$(case-insensitive).
Scoring System:
- Exact match in whitelist: +100 points → Spare Part.
- Fuzzy match >80%: +50 points.
- Fuzzy match 70-80%: +30 points.
- Found in consumable exclusion list: -100 points → Consumable.
- Threshold: Score ≥ 40 → Spare Part; < 40 → Unknown/Consumable.
3. AI Prompt Enhancement: Gemini 2.0 Flash & Claude 3.5 Sonnet
Current State
- Gemini prompt: Located in
backend/ai/prompts/gemini_extraction_prompt.md. - Claude prompt: Located in
backend/ai/prompts/claude_extraction_prompt.md. - Both focus on OCR extraction from label images.
Phase 4.1 Enhancements
New Classification Logic to Add:
Insert into both prompts a new section after Category extraction:
CLASSIFICATION GUIDE - SPARE PARTS vs CONSUMABLES:
Spare Parts (replaceable components that plug into or interface with devices):
- RAM, DDR memory modules
- SSDs, NVMe drives, M.2 modules
- CPUs, GPUs, processors
- Power supply units (PSU), power modules
- Expansion cards (PCIe, RAID, NIC)
- Cooling solutions (heatsinks, coolers)
- Motherboards
NOT Spare Parts (consumables, generic items):
- Cables (power, SATA, USB, Ethernet)
- Fasteners (screws, washers, standoffs)
- Thermal paste, thermal pads, adhesives
- Connectors, plugs, sockets
- Generic cords and adapters
Decision Tree:
1. Does the item have a replaceable function in a larger system?
2. Does it have a manufacturer part number and technical specifications?
3. Is it described with model/revision information?
If YES to 2+ questions: SPARE PART
If item matches consumable examples: CONSUMABLE
Otherwise: Mark as "uncertain" for human review.
Examples:
✓ "Kingston Fury 16GB DDR4-3200" → Spare Part (RAM)
✓ "Samsung 970 EVO 1TB NVMe" → Spare Part (SSD)
✓ "Intel Core i7-12700K" → Spare Part (CPU)
✗ "6ft SATA Cable" → Consumable (cable)
✗ "CPU Mounting Hardware Kit" → Consumable (fasteners)
Testing Approach for Prompt Accuracy
Validation Dataset:
- Create 20-30 labeled images of actual spare-parts and consumables.
- Test both Gemini and Claude on same dataset.
- Measure accuracy of Category classification.
- Measure accuracy of Part Number extraction.
- Iterate on prompt examples until >95% accuracy on test set.
Field Testing with Users:
- Have 3-5 field users test Phase 4.1 with real items.
- Collect feedback on search quality and auto-population accuracy.
- Measure time-to-save improvement (before vs. after search integration).
4. Search Result Parsing: CSS Selectors & Data Extraction
CSS Selectors for Google Search Results
Standard HTML structure (may change):
div.g // Result container
├── h3 (or a[data-sokoban-click]) // Title
├── a[href^='http'] // URL link
├── div.VwiC3b (or similar) // Snippet/description
└── div.eFM0qc // Display URL
Bing Search Selectors (more stable):
li.b_algo // Result container
├── h2 a // Title + link
├── p // Snippet
└── .tMee // Display URL
Spec Extraction from Snippets
Regex Patterns:
# Memory
r'\b(\d+)\s*(GB|TB)\s*(DDR\d|DRAM|RAM|SDRAM)'
# Storage
r'\b(\d+)\s*(GB|TB)\s*(SSD|NVME|NVMe|M\.2|HDD|SATA)'
# Processor
r'(Intel|AMD)\s+([A-Z0-9-]+)\s*(\d+\.\d+\s*GHz)?'
# Power
r'\b(\d+)\s*(W|watts?|watt)\s*(power|supply|PSU)'
# Speed/Latency
r'(\d+)\s*(MHz|GHz|CAS|Latency)'
Data Extraction Pipeline
Example Input:
Title: Kingston Fury 16GB DDR4-3200 RAM Memory Module
Snippet: Kingston Fury 16GB DDR4 3200MHz CAS Latency 16 - Get superior performance
with Kingston FURY DDR4 memory. 16GB modules deliver rock-solid stability...
Expected Output:
{
"manufacturer": "Kingston",
"model": "Fury",
"capacity": "16GB",
"memory_type": "DDR4",
"speed": "3200MHz",
"latency": "CAS 16",
"confidence": 0.95
}
Mapping to Item Fields:
Item.Name:[Kingston] [Fury] [16GB] [DDR4-3200](cleaned)Item.Category: "RAM" (from whitelist match)Item.Type: "Memory Module" or "DDR4" (spareable)Item.Notes: Full specs: "Kingston Fury 16GB DDR4-3200MHz CAS Latency 16"
Handling Variations & Abbreviations
Common variations to normalize:
- "DDR4" ↔ "DDR 4" ↔ "DDR-4"
- "3200 MHz" ↔ "3200MHz" ↔ "3.2 GHz"
- "Intel i7" ↔ "Intel Core i7" ↔ "Intel Core™ i7"
- Manufacturers: "SK Hynix" ↔ "SK Hynix" (normalize spacing)
Confidence scoring:
- Exact part number match: +0.2
- All major specs found: +0.3
- Manufacturer + model: +0.2
- Consistency checks (price matches category): +0.25
5. Backend Integration Architecture
New Modules to Create
-
backend/services/spare_parts_search.py- Main orchestrator service.
- Public methods:
search_and_extract(part_number, category, timeout=20). - Returns:
SparePartSearchResultdataclass.
-
backend/services/web_scraper.py- HTTP requests with User-Agent rotation and rate limiting.
- Methods:
search_google(),search_bing(),fetch_and_parse_html().
-
backend/services/spec_extractor.py- Regex parsing and data extraction.
- Methods:
extract_specs_from_snippet(),extract_specs_from_html().
-
backend/config/spare_parts_whitelist.py- Configurable category whitelist and exclusion patterns.
- Easy to update without code changes.
Integration Flow in /api/onboarding/extract
Current Flow:
1. User uploads image
2. AI extraction (Gemini/Claude)
3. Return extracted data to frontend
Phase 4.1 New Flow:
1. User uploads image
2. AI extraction (Gemini/Claude)
3. Check: category in whitelist AND part_number exists?
YES → Trigger async search
NO → Return AI data, skip search
4. Search executes (up to 30s timeout):
- Try Google search
- Fallback to Bing if Google fails
- Parse results, extract specs
5. Return: {
ai_data: {...},
search_results: {...} | null,
search_status: "success" | "timeout" | "error" | "skipped",
search_error: string | null
}
6. Frontend handles loading state, pre-populates fields
Search Service Pseudocode
async def search_and_extract(
part_number: str,
category: str,
manufacturer: str | None = None,
timeout: int = 20
) -> SparePartSearchResult:
"""
Search for spare part specs and extract data.
Returns immediately if timeout exceeded.
"""
try:
# Build search query
query = f"{part_number} {category} {manufacturer or ''}"
# Attempt search with timeout
with asyncio.timeout(timeout):
# Try Google first (with rate limiting)
results = await search_google(query)
if not results:
# Fallback to Bing
results = await search_bing(query)
if not results:
return SparePartSearchResult(
status="no_results",
specs=None,
error="No search results found"
)
# Parse best result
specs = extract_specs_from_snippet(results[0])
return SparePartSearchResult(
status="success",
specs=specs,
error=None,
confidence=specs.get("confidence", 0.0)
)
except asyncio.TimeoutError:
return SparePartSearchResult(
status="timeout",
specs=None,
error="Search exceeded 20s timeout"
)
except Exception as e:
return SparePartSearchResult(
status="error",
specs=None,
error=str(e)
)
Rate Limiting Implementation
Token Bucket Algorithm:
class SearchRateLimiter:
def __init__(self, requests_per_second: float = 0.2):
# 0.2 req/sec = 1 req per 5 seconds
self.capacity = 1.0
self.refill_rate = requests_per_second
self.tokens = 1.0
self.last_refill = time.time()
async def acquire(self):
"""Block until search quota available."""
while self.tokens < 1.0:
elapsed = time.time() - self.last_refill
self.tokens += elapsed * self.refill_rate
self.last_refill = time.time()
if self.tokens < 1.0:
await asyncio.sleep(0.1)
self.tokens -= 1.0
6. Frontend AIOnboarding Integration
State Additions
interface AIOnboardingState {
// ... existing state ...
isSearching: boolean; // Search in progress
searchError: string | null; // Error message if failed
searchResults: SparePartSpecs | null; // Extracted specs
searchTimeout: number; // Configurable timeout (30s default)
}
UI Flow
Sequence:
- User confirms item after AI extraction review.
- Frontend calls
POST /api/onboarding/extractwith image. - Backend returns
{ai_data, search_results, search_status, search_error}. - If search_status = "success":
- Show
"Searching for specifications..."modal (non-dismissible). - Spinner animation + countdown timer.
- Pre-populate Item.Category, Item.Type, Item.Notes from search results.
- Show
- User reviews all fields (can edit any field).
- User clicks Save to commit to database.
On Search Error:
- Show modal:
"Search failed: [error message]" - Buttons:
[Retry Search] [Skip and Save] - If Retry: Re-trigger search (max 2 retries).
- If Skip: Use AI-extracted data only.
Loading State Design
export function SearchLoadingModal({
isOpen,
timeout = 30,
onTimeout,
}: Props) {
const [secondsElapsed, setSecondsElapsed] = useState(0);
useEffect(() => {
if (!isOpen) return;
const interval = setInterval(() => {
setSecondsElapsed((prev) => {
if (prev >= timeout) {
onTimeout();
return prev;
}
return prev + 1;
});
}, 1000);
return () => clearInterval(interval);
}, [isOpen, timeout, onTimeout]);
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg max-w-md text-center">
<Spinner className="mx-auto mb-4" />
<p className="text-lg font-normal mb-2">Searching for specifications...</p>
<p className="text-sm text-slate-500">
{secondsElapsed}s / {timeout}s
</p>
</div>
</div>
);
}
Error Handling UI
function SearchErrorModal({
error,
onRetry,
onSkip,
}: Props) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-8 rounded-lg max-w-md">
<AlertCircle className="text-rose-500 mb-4 mx-auto" />
<p className="text-lg font-normal mb-4">Search failed</p>
<p className="text-sm text-slate-600 mb-6">{error}</p>
<div className="flex gap-3">
<button onClick={onRetry} className="flex-1 bg-primary text-white px-4 py-2 rounded">
Retry Search
</button>
<button onClick={onSkip} className="flex-1 border border-slate-300 px-4 py-2 rounded">
Skip
</button>
</div>
</div>
</div>
);
}
7. Performance & Scalability Analysis
Expected Latency Profile
| Component | Duration | Notes |
|---|---|---|
| AI extraction (Gemini/Claude) | 2-5s | Existing, cached |
| Network request + HTML fetch | 2-8s | Highest variability |
| HTML parsing (BeautifulSoup) | 100-500ms | |
| Regex spec extraction | 10-50ms | |
| Total end-to-end | 3-15s | Typical 20s with retries |
Handling Multiple Concurrent Searches
Recommendation: Sequential Processing
- Process searches 1 at a time with 5-second delays between.
- Prevents IP blocking and maintains consistent latency.
- Max concurrent searches: 2-3 across all users.
Implementation:
# Global search queue
search_queue: asyncio.Queue = asyncio.Queue()
async def process_search_queue():
"""Background task: process queued searches sequentially."""
while True:
search_task = await search_queue.get()
try:
await search_and_extract(**search_task)
finally:
await asyncio.sleep(5) # Rate limit between searches
search_queue.task_done()
Offline Graceful Degradation
If no internet / search fails:
- Catch all network exceptions.
- Return AI-extracted data only.
- Show UI message:
"Offline mode: using AI extraction only" - User proceeds with AI data (no pre-population from web search).
- Optional: Queue search for retry when connection restored.
Rate Limiting to Avoid IP Blocks
Per-IP Limits:
- Max 20 requests/minute to Google (distributed across all users).
- Max 10 searches per user per minute.
Backoff Strategy:
- First failure: Wait 2 seconds, retry once.
- Second failure: Wait 10 seconds, mark IP as rate-limited.
- If rate-limited: Return AI data, skip search for next 5 minutes.
User-Agent Rotation:
- Rotate User-Agent on every request (10+ pool).
- Prevents obvious bot detection.
Caching Strategy
Cache by (part_number, category) for 24 hours:
@cache.cached(timeout=86400, key_prefix="spare_parts_search:")
async def search_and_extract(part_number: str, category: str) -> SparePartSearchResult:
# Expensive search operation
Benefits:
- Repeated searches for same part (e.g., "16GB RAM DDR4") hit cache.
- Reduces network load and IP block risk.
- Improves UX (instant pre-population on cached searches).
Scalability Ceiling
Current Estimate:
- Suitable for 50-100 item onboardings per day (10-20 searches/day).
- Bottleneck: Google's IP blocking at ~20 requests/minute sustained.
To Scale Beyond 100+ Searches/Day:
- Switch to SerpAPI ($50-200/month for high volume).
- Implement proxy rotation (cost-effective, ~$5-20/month).
- Use manufacturer APIs directly (Crucial, Kingston, Corsair offer product APIs).
Architecture Diagram
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ AIOnboarding Component │
│ │
│ [Image Upload] → [AI Extraction] → [Confirm Item] │
│ │ │
│ v │
│ [Show Search Loading Modal] │
│ "Searching for specs..." (30s max) │
│ │ │
│ (on complete/error/timeout) │
│ │ │
│ [Pre-populate Fields] ← [Search Results] │
│ Category / Type / Notes editable │
│ │ │
│ v │
│ [User Reviews & Confirms] │
│ │ │
│ v │
│ POST /api/onboarding/save │
└─────────────────────────────────────────────────────────────┘
│
│ HTTP Request
v
┌──────────────────────────────────────────────────────────────┐
│ Backend (FastAPI) │
│ │
│ POST /api/onboarding/extract │
│ ├─ AI Extract (Gemini/Claude) │
│ ├─ Check: category in whitelist + part_number? │
│ └─ If YES: Call spare_parts_search.search_and_extract() │
│ │ │
│ v │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ SparePartsSearch Service │ │
│ │ │ │
│ │ Rate Limiter (token bucket, 0.2 req/sec) │ │
│ │ │ │ │
│ │ v │ │
│ │ WebScraper (requests + User-Agent rotation) │ │
│ │ ├─ search_google(query, timeout=10s) │ │
│ │ └─ search_bing(query) [fallback] │ │
│ │ │ │ │
│ │ v │ │
│ │ SpecExtractor (BeautifulSoup + regex) │ │
│ │ ├─ Parse HTML → CSS selectors │ │
│ │ ├─ Extract snippets │ │
│ │ └─ Regex extraction: specs, manufacturer, etc. │ │
│ │ │ │
│ │ Cache (24h): (part_number, category) → specs │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ v │
│ POST /api/onboarding/save │
│ ├─ Save AI data + search results to Item │
│ └─ Log to AuditLog │
└──────────────────────────────────────────────────────────────┘
Risk Mitigation Strategies
| Risk | Impact | Mitigation |
|---|---|---|
| Google IP blocking | Search fails, no specs | Use Bing fallback, implement proxy rotation, cache results |
| Network timeout | Slow UX, user frustration | 30s max timeout, show progress, fallback to AI data |
| Parsing failures (HTML changes) | No spec extraction | Update regex patterns, use manufacturer APIs, human review |
| Rate limiting abuse | Service degradation | Token bucket, per-user limits, exponential backoff |
| Search quality issues | Wrong specs populated | Confidence scoring, human review before save, field editability |
| Offline (no internet) | Feature unavailable | Graceful degradation, return AI data only, queue for retry |
Testing & Validation Strategy
Unit Tests
File: backend/tests/test_spare_parts_search.py
def test_search_and_extract_success():
"""Test successful search and spec extraction."""
result = await search_and_extract(
part_number="Kingston Fury 16GB",
category="RAM"
)
assert result.status == "success"
assert result.specs["manufacturer"] == "Kingston"
assert result.specs["capacity"] == "16GB"
def test_search_timeout():
"""Test graceful timeout handling."""
result = await search_and_extract(
part_number="test",
category="RAM",
timeout=0.1 # Force timeout
)
assert result.status == "timeout"
assert result.specs is None
def test_whitelist_matching():
"""Test spare-part classification."""
assert classify_as_spare_part("DDR4 RAM") == True
assert classify_as_spare_part("CPU 16GB") == True
assert classify_as_spare_part("Power Cable 6ft") == False
assert classify_as_spare_part("Thermal Paste") == False
def test_spec_extraction_regex():
"""Test regex patterns for spec extraction."""
snippet = "Kingston Fury 16GB DDR4-3200 CAS 16"
specs = extract_specs_from_snippet(snippet, category="RAM")
assert specs["capacity"] == "16GB"
assert specs["memory_type"] == "DDR4"
assert specs["speed"] == "3200"
Integration Tests
File: backend/tests/test_onboarding_with_search.py
@pytest.mark.asyncio
async def test_onboarding_extract_with_search():
"""Test full onboarding flow with search integration."""
# Upload image
response = await client.post(
"/api/onboarding/extract",
files={"file": ("test_ram.jpg", image_bytes)},
data={"mode": "catalog"}
)
assert response.status_code == 200
data = response.json()
assert data["ai_data"]["category"] in ["RAM", "Memory"]
assert data["search_status"] in ["success", "timeout", "error", "skipped"]
if data["search_status"] == "success":
assert "manufacturer" in data["search_results"]
assert "specs" in data["search_results"]
Frontend Tests
File: frontend/components/__tests__/SearchLoadingModal.test.tsx
describe("SearchLoadingModal", () => {
it("displays countdown timer", () => {
render(<SearchLoadingModal isOpen timeout={30} />);
expect(screen.getByText(/Searching for specifications/)).toBeInTheDocument();
expect(screen.getByText(/0s \/ 30s/)).toBeInTheDocument();
});
it("calls onTimeout after timeout expires", async () => {
const onTimeout = vi.fn();
render(<SearchLoadingModal isOpen timeout={1} onTimeout={onTimeout} />);
await new Promise(resolve => setTimeout(resolve, 1100));
expect(onTimeout).toHaveBeenCalled();
});
});
Field Testing with Users
- Recruit 3-5 power users (heavy inventory users).
- Phase A (1 week): Manual specification lookup (baseline).
- Phase B (1 week): Test Phase 4.1 with automatic search.
- Metrics:
- Time-to-save per item (before vs. after).
- Accuracy of auto-populated fields.
- Number of user edits post-search.
- Search success rate (not timeout/error).
- Collect feedback: Desired fallback sources, UX tweaks, edge cases.